zov-tech/miniapp/assets/podbor.js

520 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
Подбор техники — 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(`<div class="podbor-screen"></div>`);
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(`
<header class="podbor-header">
<button class="podbor-back" aria-label="Назад">${ICONS.arrow_left}</button>
<div class="podbor-title">Подбор техники</div>
<div style="width:28px"></div>
</header>
`);
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(`
<div class="podbor-progress">
<div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div>
<div class="podbor-progress-meta">
<span>${labels[idx]}</span><span class="num">${idx + 1}/${total}</span>
</div>
</div>
`);
}
/* ===================== Step: intro ===================== */
function renderIntro() {
const node = el(`
<section class="podbor-step">
<h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2>
<p class="lede">7 коротких шагов. Указываем категории, бюджет, инфраструктуру и предпочтения. Дальше AI сам соберёт предложение.</p>
<div class="form-row">
<label class="field">
<span class="field-label">Клиент</span>
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
</label>
</div>
<div class="form-row two-col">
<label class="field">
<span class="field-label">Телефон</span>
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ...">
</label>
<label class="field">
<span class="field-label">Бюджет на технику, ₽</span>
<input type="number" data-bind="budget_total" value="${state.budget_total || ""}" placeholder="например 350000">
</label>
</div>
<div class="podbor-cta-row">
<button class="btn-primary" data-go="categories">Начать</button>
</div>
</section>
`);
bindInputs(node);
bindNav(node);
return node;
}
/* ===================== Step: categories ===================== */
function renderCategories() {
const grid = PODBOR_CATEGORIES.map(c => `
<button class="cat-card${state.categories.includes(c.key) ? " active" : ""}" data-cat="${c.key}">
<div class="cat-icon">${ICONS[c.icon] || ""}</div>
<div class="cat-label">${c.label}</div>
${state.categories.includes(c.key) ? `<div class="cat-check">${ICONS.check}</div>` : ""}
</button>
`).join("");
const node = el(`
<section class="podbor-step">
<h2 class="display-title">Какую технику<br><span class="accent">подбираем?</span></h2>
<p class="lede">Выберите все категории, что нужно подобрать клиенту.</p>
<div class="cat-grid">${grid}</div>
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="intro">Назад</button>
<button class="btn-primary" data-go="context">Дальше</button>
</div>
</section>
`);
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 `
<div class="niche-row">
<div class="niche-label">${cat.label}</div>
<div class="niche-inputs">
<input type="number" data-niche="${c}.w" value="${n.w || ""}" placeholder="Ш, мм">
<input type="number" data-niche="${c}.h" value="${n.h || ""}" placeholder="В, мм">
<input type="number" data-niche="${c}.d" value="${n.d || ""}" placeholder="Г, мм">
</div>
</div>
`;
}).join("");
const budgets = state.categories.map(c => {
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
const v = state.budget_by_cat[c] || "";
return `
<label class="field-inline">
<span>${cat.label}</span>
<input type="number" data-budget="${c}" value="${v}" placeholder="₽">
</label>
`;
}).join("");
const node = el(`
<section class="podbor-step">
<h2 class="display-title">Размеры<br><span class="accent">и бюджет</span></h2>
<p class="lede">Если планируется встройка — укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.</p>
${niches ? `
<div class="block">
<div class="block-head">Ниши под встройку, мм</div>
<div class="niche-list">${niches}</div>
</div>
` : ""}
${budgets ? `
<div class="block">
<div class="block-head">Бюджет по категориям, ₽</div>
<div class="budget-list">${budgets}</div>
</div>
` : ""}
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="categories">Назад</button>
<button class="btn-primary" data-go="infra">Дальше</button>
</div>
</section>
`);
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(`
<section class="podbor-step">
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
<p class="lede">Газ или электрика — главный вопрос для варочной. Шахта вентиляции — для вытяжки.</p>
<div class="block">
<div class="block-head">Подключение варочной</div>
<div class="opt-list">
${PODBOR_INFRA.stove.map(o => `
<button class="opt${state.infra.stove === o.key ? " on" : ""}" data-infra="stove" data-val="${o.key}">${o.label}</button>
`).join("")}
</div>
</div>
<div class="block">
<div class="block-head">Вентиляция для вытяжки</div>
<div class="opt-list">
${PODBOR_INFRA.vent.map(o => `
<button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button>
`).join("")}
</div>
</div>
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="context">Назад</button>
<button class="btn-primary" data-go="scenario">Дальше</button>
</div>
</section>
`);
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(`
<section class="podbor-step">
<h2 class="display-title">Сценарий<br><span class="accent">использования</span></h2>
<p class="lede">Семья с детьми готовит иначе, чем пара. AI учтёт это в подборе.</p>
<div class="block">
<div class="block-head">Состав семьи</div>
<div class="opt-list">
${PODBOR_FAMILY.map(o => `
<button class="opt${state.scenario.family === o.key ? " on" : ""}" data-scenario="family" data-val="${o.key}">${o.label}</button>
`).join("")}
</div>
</div>
<div class="block">
<div class="block-head">Частота готовки</div>
<div class="opt-list">
${PODBOR_COOKING.map(o => `
<button class="opt${state.scenario.cooking === o.key ? " on" : ""}" data-scenario="cooking" data-val="${o.key}">${o.label}</button>
`).join("")}
</div>
</div>
<div class="block">
<div class="block-head">Любимые техники приготовления</div>
<div class="opt-list">
${PODBOR_TECHNIQUES.map(o => `
<button class="opt${(state.scenario.techniques || []).includes(o.key) ? " on" : ""}" data-tech="${o.key}">${o.label}</button>
`).join("")}
</div>
<div class="hint">Можно несколько</div>
</div>
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="infra">Назад</button>
<button class="btn-primary" data-go="brands">Дальше</button>
</div>
</section>
`);
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(`<section class="podbor-step"><div class="empty">Сначала выберите категории.</div></section>`);
}
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) => `
<div class="tier-row">
<div class="tier-label">${PODBOR_BUDGET_TIERS.find(t => t.key === tier).label}</div>
<div class="brand-chips">
${(brands[tier] || []).map(b => {
const status = catState[b] || "none";
return `<button class="chip status-${status}" data-cat="${catKey}" data-brand="${b}">${b}</button>`;
}).join("")}
</div>
</div>
`;
return `
<div class="block">
<div class="block-head">${cat.label}</div>
<div class="hint">★ Тап — предпочтительно · ✓ Двойной — допустимо · — Третий — снять</div>
${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")}
</div>
`;
}).join("");
const node = el(`
<section class="podbor-step">
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
<p class="lede">Какие марки уважаете, какие — допустимы. AI сначала пробует preferred.</p>
${blocks}
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="scenario">Назад</button>
<button class="btn-primary" data-go="summary">Дальше</button>
</div>
</section>
`);
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(`
<section class="podbor-step">
<h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2>
<p class="lede">Проверьте и отправьте — AI вернёт предложение в чат с ботом.</p>
<div class="block summary-block">
<div class="kv"><span>Клиент</span><strong>${state.client_name || "—"}</strong></div>
<div class="kv"><span>Бюджет</span><strong>${state.budget_total ? state.budget_total + " ₽" : "—"}</strong></div>
<div class="kv"><span>Категорий</span><strong>${state.categories.length}</strong></div>
<div class="kv"><span>Семья</span><strong>${PODBOR_FAMILY.find(f => f.key === state.scenario.family)?.label || "—"}</strong></div>
<div class="kv"><span>Готовка</span><strong>${PODBOR_COOKING.find(f => f.key === state.scenario.cooking)?.label || "—"}</strong></div>
<div class="kv"><span>Подключение</span><strong>${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || "—"}</strong></div>
</div>
<label class="field">
<span class="field-label">Дополнительные пожелания</span>
<textarea data-bind="notes" rows="3" placeholder="Что-то особенное от клиента?">${state.notes || ""}</textarea>
</label>
<div class="podbor-cta-row">
<button class="btn-secondary" data-go="brands">Назад</button>
<button class="btn-primary" id="submitBtn">Отправить · AI подберёт</button>
</div>
<div id="submitResult" class="submit-result"></div>
</section>
`);
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 = '<span class="spinner-inline"></span> AI думает...';
result.innerHTML = "";
if (!BACKEND_URL) {
result.innerHTML = `<div class="error">BACKEND_URL не настроен (dev-режим).</div>`;
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 = `<div class="error">Ошибка: ${data.error}</div>`;
} else {
result.innerHTML = `
<div class="success">
<div class="success-icon">${ICONS.check}</div>
<div>
<div class="success-title">Подбор отправлен в чат бота</div>
<div class="success-sub">Лид #${(data.id || "").slice(0, 6)} · откройте Telegram</div>
</div>
</div>
`;
haptic && haptic("success");
}
} catch (e) {
result.innerHTML = `<div class="error">Сеть: ${e.message}</div>`;
}
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(); } };
})();