mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 16:24:50 +00:00
miniapp: new pricing flow — brand strategy + budget presets + multi pick strategy
NEW STRUCTURE: - Step 4 'Бренд' — ai/single/different + brand picker or per-cat chips (now 4-state with 'avoid') - Step 5 'Бюджет' — Люкс/Премиум/Средний/Бюджет/Точные цифры presets - Step 6 'Стратегия' — multi: Лучшее по отзывам / Цена-качество / Топ-бренды / Доступное / Tech / Стиль - Step 7 'Инфра' — перенесено после стратегии - Step 8 'Итог' — обновлённый summary с новыми полями FIXES: - Keyboard-disappearing bug in price inputs — removed render() on input, total recomputed locally - localStorage merge with defaults for backward compat with new fields - Bumped STORAGE_KEY to v4 REMAINING: - Backend still reads checklist.priorities (old shape) — needs update to read pick_strategies + brand_strategy + budget_preset
This commit is contained in:
parent
496ddf793c
commit
dd400b71ac
@ -42,6 +42,49 @@ const PODBOR_PRIORITIES = [
|
||||
{ key: "service", label: "Сервис и гарантия" },
|
||||
];
|
||||
|
||||
/* === Новая структура: бренд-стратегия / бюджет / стратегия подбора === */
|
||||
|
||||
const PODBOR_BRAND_STRATEGY = [
|
||||
{ key: "ai", label: "Пусть AI решит", hint: "оптимально под бюджет и стратегию", recommended: true },
|
||||
{ key: "single", label: "Одна марка на всю кухню", hint: "моноблочный комплект, премиум-сценарий" },
|
||||
{ key: "different", label: "Разные марки по категориям", hint: "соберём оптимальный микс" },
|
||||
];
|
||||
|
||||
/* Бренды, у которых есть полная линейка кухонной техники (для single-mode) */
|
||||
const PODBOR_SINGLE_BRAND_OPTIONS = [
|
||||
{ key: "miele", label: "Miele", tier: "premium" },
|
||||
{ key: "gaggenau", label: "Gaggenau", tier: "premium" },
|
||||
{ key: "asko", label: "Asko", tier: "premium" },
|
||||
{ key: "v_zug", label: "V-ZUG", tier: "premium" },
|
||||
{ key: "neff", label: "Neff", tier: "middle" },
|
||||
{ key: "bosch", label: "Bosch", tier: "middle" },
|
||||
{ key: "siemens", label: "Siemens", tier: "middle" },
|
||||
{ key: "electrolux", label: "Electrolux", tier: "middle" },
|
||||
{ key: "aeg", label: "AEG", tier: "middle" },
|
||||
{ key: "samsung", label: "Samsung", tier: "middle" },
|
||||
{ key: "lg", label: "LG", tier: "middle" },
|
||||
{ key: "hansa", label: "Hansa", tier: "budget" },
|
||||
{ key: "beko", label: "Beko", tier: "budget" },
|
||||
{ key: "ai_pick", label: "Пусть AI выберет под бюджет", recommended: true },
|
||||
];
|
||||
|
||||
const PODBOR_BUDGET_PRESETS = [
|
||||
{ key: "luxe", label: "Люкс", hint: "от 1.5М ₽ за весь комплект" },
|
||||
{ key: "premium", label: "Премиум", hint: "700к – 1.5М ₽" },
|
||||
{ key: "middle", label: "Средний", hint: "350к – 700к ₽", recommended: true },
|
||||
{ key: "budget", label: "Бюджет", hint: "до 350к ₽" },
|
||||
{ key: "exact", label: "Точные цифры", hint: "ввести от-до по категориям" },
|
||||
];
|
||||
|
||||
const PODBOR_PICK_STRATEGIES = [
|
||||
{ key: "reviews", label: "Лучшее по отзывам", hint: "топ по рейтингам пользователей" },
|
||||
{ key: "balance", label: "Цена / качество", hint: "оптимальный баланс", recommended: true },
|
||||
{ key: "premium_brand", label: "Топ-бренды премиум", hint: "Miele · Gaggenau · Sub-Zero" },
|
||||
{ key: "cheap", label: "Самое доступное", hint: "надёжный минимум" },
|
||||
{ key: "tech", label: "Современные технологии", hint: "Wi-Fi · инверторы · пар" },
|
||||
{ key: "style", label: "Стилевая согласованность", hint: "единый дизайн-язык всей техники" },
|
||||
];
|
||||
|
||||
/* Параметры по категориям.
|
||||
----------------------------------------------------------
|
||||
Новая схема (иерархический wizard):
|
||||
|
||||
@ -486,6 +486,24 @@
|
||||
|
||||
.chip.status-acceptable::before { content: "✓ "; }
|
||||
|
||||
/* Состояние avoid — приглушённый, перечёркнутый */
|
||||
.chip.status-avoid {
|
||||
background: #F5E1DC;
|
||||
border-color: #C7705A;
|
||||
color: #8A3E2A;
|
||||
text-decoration: line-through;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.chip.status-avoid::before { content: "✗ "; text-decoration: none; display: inline-block; }
|
||||
|
||||
/* Disabled кнопка */
|
||||
.btn-primary[disabled],
|
||||
.btn-secondary[disabled] {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ----- Summary ----- */
|
||||
.summary-block { gap: 8px; }
|
||||
.summary-block .kv {
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
============================================================ */
|
||||
|
||||
const Podbor = (function () {
|
||||
const STORAGE_KEY = "zov-podbor-v3";
|
||||
const STEPS = ["intro", "categories", "detail", "pricing", "infra", "priorities", "brands", "summary"];
|
||||
const STORAGE_KEY = "zov-podbor-v4";
|
||||
const STEPS = ["intro", "categories", "detail", "brand", "budget", "strategy", "infra", "summary"];
|
||||
const STEP_LABELS = ["Старт", "Категории", "Параметры", "Бренд", "Бюджет", "Стратегия", "Инфра", "Итог"];
|
||||
|
||||
// Внутренний sub-state для шага «detail»: 'menu' | 'cat:<key>'
|
||||
let detailView = "menu";
|
||||
@ -16,7 +17,11 @@ const Podbor = (function () {
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
// Мерж с дефолтами для совместимости с новыми полями
|
||||
return { ...defaultState(), ...parsed };
|
||||
}
|
||||
} catch (e) {}
|
||||
return defaultState();
|
||||
}
|
||||
@ -27,11 +32,14 @@ const Podbor = (function () {
|
||||
client_phone: "",
|
||||
address: "",
|
||||
categories: [], // ['fridge','hob',...]
|
||||
per_cat: {}, // { fridge: { params: {type:'sbs',...}, features: ['nofrost'], notes: '' }, ... }
|
||||
price_ranges: {}, // { fridge: { from: 50000, to: 120000 }, ... }
|
||||
per_cat: {}, // { fridge: { answers: {install:'built_in',...}, notes: '', _step: 0 } }
|
||||
brand_strategy: "", // 'ai' | 'single' | 'different'
|
||||
single_brand: "", // key из PODBOR_SINGLE_BRAND_OPTIONS, если brand_strategy === 'single'
|
||||
brands: {}, // если brand_strategy === 'different' — { fridge: {Bosch:'preferred'|'acceptable'|'avoid'} }
|
||||
budget_preset: "", // 'luxe'|'premium'|'middle'|'budget'|'exact'
|
||||
price_ranges: {}, // только если budget_preset === 'exact'
|
||||
pick_strategies: [], // ['reviews','balance','tech',...] — multi
|
||||
infra: { stove: "", vent: "" },
|
||||
priorities: [], // ['balance','reviews',...]
|
||||
brands: {}, // { fridge: {Bosch:'preferred',...}, ... }
|
||||
notes: "",
|
||||
};
|
||||
}
|
||||
@ -78,10 +86,10 @@ const Podbor = (function () {
|
||||
case "intro": screen.appendChild(renderIntro()); break;
|
||||
case "categories": screen.appendChild(renderCategories()); break;
|
||||
case "detail": screen.appendChild(renderDetail()); break;
|
||||
case "pricing": screen.appendChild(renderPricing()); break;
|
||||
case "brand": screen.appendChild(renderBrand()); break;
|
||||
case "budget": screen.appendChild(renderBudget()); break;
|
||||
case "strategy": screen.appendChild(renderStrategy()); break;
|
||||
case "infra": screen.appendChild(renderInfra()); break;
|
||||
case "priorities": screen.appendChild(renderPriorities()); break;
|
||||
case "brands": screen.appendChild(renderBrands()); break;
|
||||
case "summary": screen.appendChild(renderSummary()); break;
|
||||
}
|
||||
}
|
||||
@ -148,12 +156,11 @@ const Podbor = (function () {
|
||||
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>
|
||||
<span>${STEP_LABELS[idx] || ""}</span><span class="num">${idx + 1}/${total}</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@ -292,7 +299,7 @@ const Podbor = (function () {
|
||||
<div class="detail-list">${cards}</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||
<button class="btn-primary" data-go="pricing">Дальше</button>
|
||||
<button class="btn-primary" data-go="brand">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
@ -700,27 +707,165 @@ const Podbor = (function () {
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: pricing (ценовой коридор по категориям) ===================== */
|
||||
function formatRub(n) {
|
||||
if (!n) return "—";
|
||||
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
}
|
||||
|
||||
function renderPricing() {
|
||||
if (!state.categories.length) {
|
||||
return el(`
|
||||
<section class="podbor-step">
|
||||
<div class="empty">Сначала выберите категории.</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="detail">Назад</button>
|
||||
/* Универсальный рендер пин-карточек (label + hint, single или multi) */
|
||||
function renderPinCards(items, getStatus, onClick, opts = {}) {
|
||||
const html = items.map(o => {
|
||||
const status = getStatus(o); // 'on' | 'on-star' | ''
|
||||
const isOn = status === "on" || status === "on-star";
|
||||
const cls = "wiz-card wiz-card--pin" + (isOn ? " on" : "") + (o.recommended ? " star" : "");
|
||||
return `
|
||||
<button class="${cls}" data-key="${o.key}">
|
||||
<span class="wiz-label">${o.label}</span>
|
||||
${o.hint ? `<span class="wiz-hint">${o.hint}</span>` : ""}
|
||||
${isOn ? `<span class="wiz-tick">${ICONS.check}</span>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
const wrap = el(`<div class="wiz-grid wiz-grid--pins">${html}</div>`);
|
||||
wrap.querySelectorAll(".wiz-card").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
onClick(btn.dataset.key);
|
||||
});
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/* ===================== Step: brand (бренд-стратегия + выбор) ===================== */
|
||||
|
||||
function renderBrand() {
|
||||
const bs = state.brand_strategy || "";
|
||||
const strategyGrid = renderPinCards(
|
||||
PODBOR_BRAND_STRATEGY,
|
||||
o => (bs === o.key ? "on" : ""),
|
||||
key => { update({ brand_strategy: key }); render(); }
|
||||
);
|
||||
|
||||
// Подблок зависит от выбранной стратегии
|
||||
let subBlock = "";
|
||||
if (bs === "single") {
|
||||
const sb = state.single_brand || "";
|
||||
const cardsHtml = PODBOR_SINGLE_BRAND_OPTIONS.map(o => {
|
||||
const on = sb === o.key;
|
||||
return `
|
||||
<button class="wiz-card wiz-card--pin${on ? " on" : ""}${o.recommended ? " star" : ""}" data-sb="${o.key}">
|
||||
<span class="wiz-label">${o.label}</span>
|
||||
${o.tier ? `<span class="wiz-hint">${tierLabel(o.tier)}</span>` : ""}
|
||||
${on ? `<span class="wiz-tick">${ICONS.check}</span>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
subBlock = `
|
||||
<div class="block">
|
||||
<div class="block-head">Какая марка</div>
|
||||
<div class="wiz-grid wiz-grid--pins">${cardsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (bs === "different") {
|
||||
// Чипы по категориям с 4-state статусами (none → preferred → acceptable → avoid → none)
|
||||
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 tierGroup = (tier) => `
|
||||
<div class="brand-chips brand-tier-${tier}">
|
||||
${(brands[tier] || []).map(b => {
|
||||
const status = catState[b] || "none";
|
||||
return `<button class="chip tier-${tier} status-${status}" data-cat="${catKey}" data-brand="${b}">${b}</button>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
`;
|
||||
return `
|
||||
<div class="block">
|
||||
<div class="block-head">${cat.label}</div>
|
||||
${tierGroup("premium")}${tierGroup("middle")}${tierGroup("budget")}
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
subBlock = `
|
||||
<div class="hint">Тап — ★ хочу · повторно — ✓ согласен · третий — ✗ не хочу · четвёртый — снять</div>
|
||||
${blocks}
|
||||
`;
|
||||
} else if (bs === "ai") {
|
||||
subBlock = `
|
||||
<div class="block">
|
||||
<div class="hint">AI подберёт оптимальный микс брендов под выбранный бюджет и стратегию. Можно ничего больше не указывать.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Бренд<br><span class="accent">стратегия</span></h2>
|
||||
<p class="lede">Хочет ли клиент всю технику от одной марки, или собираем оптимальный микс?</p>
|
||||
</section>
|
||||
`);
|
||||
node.appendChild(strategyGrid);
|
||||
if (subBlock) {
|
||||
const sub = el(`<div>${subBlock}</div>`);
|
||||
node.appendChild(sub);
|
||||
// Single-brand chips
|
||||
sub.querySelectorAll("[data-sb]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
update({ single_brand: b.dataset.sb });
|
||||
render();
|
||||
});
|
||||
});
|
||||
// Different-brand 4-state cycle
|
||||
sub.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"
|
||||
: cur === "acceptable" ? "avoid"
|
||||
: "none";
|
||||
const catBrands = { ...(state.brands[catKey] || {}) };
|
||||
if (nextStatus === "none") delete catBrands[brand];
|
||||
else catBrands[brand] = nextStatus;
|
||||
update({ brands: { ...state.brands, [catKey]: catBrands } });
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Подсчёт суммы коридоров
|
||||
const cta = el(`
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="detail">Назад</button>
|
||||
<button class="btn-primary" data-go="budget"${bs ? "" : " disabled"}>Дальше</button>
|
||||
</div>
|
||||
`);
|
||||
node.appendChild(cta);
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function tierLabel(tier) {
|
||||
return tier === "premium" ? "премиум" : tier === "middle" ? "средний" : tier === "budget" ? "бюджет" : "";
|
||||
}
|
||||
|
||||
/* ===================== Step: budget (пресет или точные цифры) ===================== */
|
||||
|
||||
function renderBudget() {
|
||||
const bp = state.budget_preset || "";
|
||||
const presetGrid = renderPinCards(
|
||||
PODBOR_BUDGET_PRESETS,
|
||||
o => (bp === o.key ? "on" : ""),
|
||||
key => { update({ budget_preset: key }); render(); }
|
||||
);
|
||||
|
||||
// Если "exact" — показываем поля от-до по категориям
|
||||
let exactBlock = null;
|
||||
if (bp === "exact") {
|
||||
let totalFrom = 0, totalTo = 0;
|
||||
state.categories.forEach(c => {
|
||||
const r = state.price_ranges[c] || {};
|
||||
if (r.from) totalFrom += parseInt(r.from, 10) || 0;
|
||||
if (r.to) totalTo += parseInt(r.to, 10) || 0;
|
||||
});
|
||||
|
||||
const rows = state.categories.map(c => {
|
||||
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
|
||||
const r = state.price_ranges[c] || {};
|
||||
@ -736,41 +881,89 @@ const Podbor = (function () {
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const totalLine = (totalFrom || totalTo)
|
||||
? `<div class="price-total">Итого: <strong>${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽</strong></div>`
|
||||
: `<div class="price-total muted">Сумма посчитается автоматически</div>`;
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Ценовой<br><span class="accent">коридор</span></h2>
|
||||
<p class="lede">«От — До» по каждой категории. AI подберёт варианты, которые попадают в коридор и совокупно укладываются в общий бюджет клиента.</p>
|
||||
exactBlock = el(`
|
||||
<div class="block">
|
||||
<div class="block-head">По категориям, ₽</div>
|
||||
<div class="price-list">${rows}</div>
|
||||
${totalLine}
|
||||
<div class="price-total" id="priceTotalLine">${
|
||||
(totalFrom || totalTo)
|
||||
? `Итого: <strong>${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽</strong>`
|
||||
: `<span class="muted">Сумма посчитается автоматически</span>`
|
||||
}</div>
|
||||
</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="detail">Назад</button>
|
||||
<button class="btn-primary" data-go="infra">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll("[data-price]").forEach(inp => {
|
||||
// Внимание: НЕ вызываем render() на input — иначе клавиатура слетает
|
||||
exactBlock.querySelectorAll("[data-price]").forEach(inp => {
|
||||
inp.addEventListener("input", e => {
|
||||
const [cat, key] = e.target.dataset.price.split(".");
|
||||
const next = { ...state.price_ranges, [cat]: { ...(state.price_ranges[cat] || {}), [key]: e.target.value } };
|
||||
update({ price_ranges: next });
|
||||
render();
|
||||
// Локально пересчитываем сумму
|
||||
let tf = 0, tt = 0;
|
||||
state.categories.forEach(c => {
|
||||
const r = state.price_ranges[c] || {};
|
||||
if (r.from) tf += parseInt(r.from, 10) || 0;
|
||||
if (r.to) tt += parseInt(r.to, 10) || 0;
|
||||
});
|
||||
const line = exactBlock.querySelector("#priceTotalLine");
|
||||
if (line) {
|
||||
line.innerHTML = (tf || tt)
|
||||
? `Итого: <strong>${formatRub(tf)} — ${formatRub(tt)} ₽</strong>`
|
||||
: `<span class="muted">Сумма посчитается автоматически</span>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Бюджет<br><span class="accent">на технику</span></h2>
|
||||
<p class="lede">Выбери диапазон. AI сам распределит бюджет по категориям (холодильник ~25%, варочная ~15%, духовка ~15% и т.д.).</p>
|
||||
</section>
|
||||
`);
|
||||
node.appendChild(presetGrid);
|
||||
if (exactBlock) node.appendChild(exactBlock);
|
||||
const cta = el(`
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="brand">Назад</button>
|
||||
<button class="btn-primary" data-go="strategy"${bp ? "" : " disabled"}>Дальше</button>
|
||||
</div>
|
||||
`);
|
||||
node.appendChild(cta);
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function formatRub(n) {
|
||||
if (!n) return "—";
|
||||
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
/* ===================== Step: strategy (что важно при подборе — multi) ===================== */
|
||||
|
||||
function renderStrategy() {
|
||||
const cur = state.pick_strategies || [];
|
||||
const grid = renderPinCards(
|
||||
PODBOR_PICK_STRATEGIES,
|
||||
o => (cur.includes(o.key) ? "on" : ""),
|
||||
key => {
|
||||
const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
|
||||
update({ pick_strategies: next });
|
||||
render();
|
||||
}
|
||||
);
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Стратегия<br><span class="accent">подбора</span></h2>
|
||||
<p class="lede">Что для клиента важно при выборе? Можно несколько — AI учтёт всё.</p>
|
||||
</section>
|
||||
`);
|
||||
node.appendChild(grid);
|
||||
const cta = el(`
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="budget">Назад</button>
|
||||
<button class="btn-primary" data-go="infra">Дальше</button>
|
||||
</div>
|
||||
`);
|
||||
node.appendChild(cta);
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: infra ===================== */
|
||||
@ -779,7 +972,7 @@ const Podbor = (function () {
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
||||
<p class="lede">Газ или электрика — определит тип варочной (индукция / стеклокерамика / газ). Подключение вытяжки — нужны ли выводы или угольный фильтр.</p>
|
||||
<p class="lede">Газ или электрика — определит тип варочной. Подключение вытяжки — нужны ли выводы или угольный фильтр.</p>
|
||||
<div class="block">
|
||||
<div class="block-head">Подключение варочной</div>
|
||||
<div class="opt-list">
|
||||
@ -798,8 +991,8 @@ const Podbor = (function () {
|
||||
<div class="hint">Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.</div>
|
||||
</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="pricing">Назад</button>
|
||||
<button class="btn-primary" data-go="priorities">Дальше</button>
|
||||
<button class="btn-secondary" data-go="strategy">Назад</button>
|
||||
<button class="btn-primary" data-go="summary">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
@ -813,110 +1006,40 @@ const Podbor = (function () {
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: priorities (что важно при выборе) ===================== */
|
||||
|
||||
function renderPriorities() {
|
||||
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_PRIORITIES.map(o => `
|
||||
<button class="opt${(state.priorities || []).includes(o.key) ? " on" : ""}" data-pri="${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-pri]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
const cur = state.priorities || [];
|
||||
const key = b.dataset.pri;
|
||||
const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
|
||||
update({ priorities: 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 tierGroup = (tier) => `
|
||||
<div class="brand-chips brand-tier-${tier}">
|
||||
${(brands[tier] || []).map(b => {
|
||||
const status = catState[b] || "none";
|
||||
return `<button class="chip tier-${tier} status-${status}" data-cat="${catKey}" data-brand="${b}" data-tier="${tier}">${b}</button>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
`;
|
||||
return `
|
||||
<div class="block">
|
||||
<div class="block-head">${cat.label}</div>
|
||||
${tierGroup("premium")}${tierGroup("middle")}${tierGroup("budget")}
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
|
||||
<p class="lede">Тап — ★ предпочтительно. Дабл — ✓ допустимо. Третий — снять. AI сначала пробует ★, потом ✓.</p>
|
||||
${blocks}
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="priorities">Назад</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 bs = state.brand_strategy;
|
||||
const bsLabel = PODBOR_BRAND_STRATEGY.find(s => s.key === bs)?.label || "—";
|
||||
let brandDetail = "";
|
||||
if (bs === "single") {
|
||||
const sb = PODBOR_SINGLE_BRAND_OPTIONS.find(o => o.key === state.single_brand);
|
||||
brandDetail = sb ? ` · ${sb.label}` : "";
|
||||
} else if (bs === "different") {
|
||||
const totalBrands = Object.values(state.brands || {}).reduce((s, c) => s + Object.keys(c || {}).length, 0);
|
||||
brandDetail = totalBrands ? ` · ${totalBrands} отметок` : "";
|
||||
}
|
||||
|
||||
// Бюджет
|
||||
const bp = state.budget_preset;
|
||||
const bpDef = PODBOR_BUDGET_PRESETS.find(p => p.key === bp);
|
||||
let budgetLabel = bpDef?.label || "—";
|
||||
if (bp === "exact") {
|
||||
let totalFrom = 0, totalTo = 0;
|
||||
state.categories.forEach(c => {
|
||||
const r = state.price_ranges[c] || {};
|
||||
totalFrom += parseInt(r.from || "0", 10) || 0;
|
||||
totalTo += parseInt(r.to || "0", 10) || 0;
|
||||
});
|
||||
const totalRange = (totalFrom || totalTo)
|
||||
? `${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽`
|
||||
: "—";
|
||||
const priorityLabels = (state.priorities || [])
|
||||
.map(k => PODBOR_PRIORITIES.find(p => p.key === k)?.label)
|
||||
if (totalFrom || totalTo) budgetLabel = `${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽`;
|
||||
} else if (bpDef?.hint) {
|
||||
budgetLabel = `${bpDef.label} · ${bpDef.hint}`;
|
||||
}
|
||||
|
||||
// Стратегия подбора
|
||||
const strategyLabels = (state.pick_strategies || [])
|
||||
.map(k => PODBOR_PICK_STRATEGIES.find(s => s.key === k)?.label)
|
||||
.filter(Boolean).join(" · ");
|
||||
|
||||
const node = el(`
|
||||
@ -926,10 +1049,11 @@ const Podbor = (function () {
|
||||
<div class="block summary-block">
|
||||
<div class="kv"><span>Клиент</span><strong>${state.client_name || "—"}</strong></div>
|
||||
<div class="kv"><span>Категорий</span><strong>${state.categories.length}</strong></div>
|
||||
<div class="kv"><span>Ценовой коридор</span><strong>${totalRange}</strong></div>
|
||||
<div class="kv"><span>Бренд</span><strong>${bsLabel}${brandDetail}</strong></div>
|
||||
<div class="kv"><span>Бюджет</span><strong>${budgetLabel}</strong></div>
|
||||
<div class="kv"><span>Стратегия</span><strong>${strategyLabels || "—"}</strong></div>
|
||||
<div class="kv"><span>Подключение</span><strong>${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || "—"}</strong></div>
|
||||
<div class="kv"><span>Вентиляция</span><strong>${PODBOR_INFRA.vent.find(f => f.key === state.infra.vent)?.label || "—"}</strong></div>
|
||||
<div class="kv"><span>Приоритеты</span><strong>${priorityLabels || "—"}</strong></div>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
@ -938,7 +1062,7 @@ const Podbor = (function () {
|
||||
</label>
|
||||
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="brands">Назад</button>
|
||||
<button class="btn-secondary" data-go="infra">Назад</button>
|
||||
<button class="btn-primary" id="submitBtn">Отправить · AI подберёт</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260511b">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511b">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260511c">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511c">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
@ -21,10 +21,10 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js?v=20260511b"></script>
|
||||
<script src="assets/podbor.config.js?v=20260511b"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260511b"></script>
|
||||
<script src="assets/podbor.js?v=20260511b"></script>
|
||||
<script src="assets/app.js?v=20260511b"></script>
|
||||
<script src="assets/icons.js?v=20260511c"></script>
|
||||
<script src="assets/podbor.config.js?v=20260511c"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260511c"></script>
|
||||
<script src="assets/podbor.js?v=20260511c"></script>
|
||||
<script src="assets/app.js?v=20260511c"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user