mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 19:04:49 +00:00
feat(podbor): drop niches; price-range from-to per cat; ventilation Y/N; priorities multi-select; brand tiers as color (no labels)
This commit is contained in:
parent
571297c017
commit
129046de07
@ -19,19 +19,6 @@ const PODBOR_BUDGET_TIERS = [
|
|||||||
{ key: "budget", label: "Бюджет", hint: "только нужное" },
|
{ key: "budget", label: "Бюджет", hint: "только нужное" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PODBOR_FAMILY = [
|
|
||||||
{ key: "single", label: "1 взрослый" },
|
|
||||||
{ key: "couple", label: "Пара" },
|
|
||||||
{ key: "family", label: "Семья с детьми" },
|
|
||||||
{ key: "multigen", label: "2+ поколения" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PODBOR_COOKING = [
|
|
||||||
{ key: "daily", label: "Ежедневно" },
|
|
||||||
{ key: "weekly", label: "3–5 раз в неделю" },
|
|
||||||
{ key: "rare", label: "По выходным или реже" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PODBOR_INFRA = {
|
const PODBOR_INFRA = {
|
||||||
stove: [
|
stove: [
|
||||||
{ key: "induction", label: "Индукция / 380 В" },
|
{ key: "induction", label: "Индукция / 380 В" },
|
||||||
@ -40,19 +27,19 @@ const PODBOR_INFRA = {
|
|||||||
{ key: "any", label: "Не знаю / любой" },
|
{ key: "any", label: "Не знаю / любой" },
|
||||||
],
|
],
|
||||||
vent: [
|
vent: [
|
||||||
{ key: "shaft", label: "Шахта вентиляции есть" },
|
{ key: "yes", label: "Да — есть выводы в вентиляцию" },
|
||||||
{ key: "no_shaft", label: "Только рециркуляция" },
|
{ key: "no", label: "Нет — рециркуляция с угольным фильтром" },
|
||||||
{ key: "unknown", label: "Не знаю" },
|
{ key: "unknown", label: "Не знаю — менеджер уточнит" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const PODBOR_TECHNIQUES = [
|
const PODBOR_PRIORITIES = [
|
||||||
{ key: "bake", label: "Выпечка" },
|
{ key: "balance", label: "Цена / качество" },
|
||||||
{ key: "steam", label: "На пару" },
|
{ key: "reviews", label: "Отзывы" },
|
||||||
{ key: "grill", label: "Гриль" },
|
{ key: "popular", label: "Популярность бренда" },
|
||||||
{ key: "wok", label: "Wok / стир-фрай" },
|
{ key: "design", label: "Дизайн и цвет" },
|
||||||
{ key: "low_t", label: "Низкотемпературное" },
|
{ key: "tech", label: "Технологичность" },
|
||||||
{ key: "smart", label: "Умные режимы / Smart" },
|
{ key: "service", label: "Сервис и гарантия" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Бренды для каждой категории — для чипов с тирами.
|
/* Бренды для каждой категории — для чипов с тирами.
|
||||||
|
|||||||
@ -258,6 +258,72 @@
|
|||||||
text-align: right; color: var(--ink);
|
text-align: right; color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----- Price range (от — до по категориям) ----- */
|
||||||
|
.price-list { display: flex; flex-direction: column; gap: var(--s3); }
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-inputs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-inputs input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-tag);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-inputs .dash {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-inputs .rub {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total {
|
||||||
|
margin-top: var(--s2);
|
||||||
|
padding-top: var(--s2);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.10em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-2);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total strong { font-family: var(--font-ui); font-size: 14px; color: var(--ink); font-weight: 600; }
|
||||||
|
.price-total.muted { color: var(--muted); }
|
||||||
|
|
||||||
/* ----- Option chips ----- */
|
/* ----- Option chips ----- */
|
||||||
.opt-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
.opt-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
|
||||||
@ -283,49 +349,86 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ----- Brand chips ----- */
|
/* ----- Brand chips ----- */
|
||||||
.tier-row { display: flex; flex-direction: column; gap: 6px; padding: 8px 0; border-top: 1px solid var(--line); }
|
/* Тиры (Premium/Middle/Budget) различаются цветом, без явных текстовых ярлыков.
|
||||||
.tier-row:first-child { border-top: none; padding-top: 0; }
|
Внутренне храним tier для аналитики «температуры» клиента. */
|
||||||
|
|
||||||
.tier-label {
|
.brand-chips {
|
||||||
font-family: var(--font-mono);
|
display: flex;
|
||||||
font-size: 9.5px;
|
flex-wrap: wrap;
|
||||||
font-weight: 500;
|
gap: 6px;
|
||||||
letter-spacing: 0.14em;
|
padding: 6px 0;
|
||||||
text-transform: uppercase;
|
border-top: 1px dashed var(--line);
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
.brand-chips:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 6px 10px;
|
padding: 6px 11px;
|
||||||
border-radius: var(--r-tag);
|
border-radius: var(--r-tag);
|
||||||
border: 1px solid var(--line-strong);
|
border: 1px solid;
|
||||||
background: var(--paper);
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.12s;
|
transition: all 0.12s;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.status-preferred {
|
/* Базовый цвет покоя по тирам — без слов, только тёплый/холодный оттенок */
|
||||||
background: var(--accent-2);
|
.chip.tier-premium {
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--accent-2); /* walnut */
|
||||||
|
border-color: rgba(107, 74, 43, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.tier-middle {
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink-2);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.tier-budget {
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--muted);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Состояние preferred — заливка цветом тира */
|
||||||
|
.chip.tier-premium.status-preferred {
|
||||||
|
background: var(--accent-2); /* walnut */
|
||||||
color: var(--paper);
|
color: var(--paper);
|
||||||
border-color: var(--accent-2);
|
border-color: var(--accent-2);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chip.tier-middle.status-preferred {
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
border-color: var(--ink);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.tier-budget.status-preferred {
|
||||||
|
background: var(--muted);
|
||||||
|
color: var(--paper);
|
||||||
|
border-color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.chip.status-preferred::before { content: "★ "; }
|
.chip.status-preferred::before { content: "★ "; }
|
||||||
|
|
||||||
|
/* Состояние acceptable — обводка цветом тира, прозрачная заливка */
|
||||||
.chip.status-acceptable {
|
.chip.status-acceptable {
|
||||||
background: var(--paper-2);
|
background: var(--paper-2);
|
||||||
color: var(--ink);
|
|
||||||
border-color: var(--accent-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chip.tier-premium.status-acceptable { border-color: var(--accent-2); color: var(--accent-2); }
|
||||||
|
.chip.tier-middle.status-acceptable { border-color: var(--ink); color: var(--ink); }
|
||||||
|
.chip.tier-budget.status-acceptable { border-color: var(--muted); color: var(--ink-2); }
|
||||||
|
|
||||||
.chip.status-acceptable::before { content: "✓ "; }
|
.chip.status-acceptable::before { content: "✓ "; }
|
||||||
|
|
||||||
/* ----- Summary ----- */
|
/* ----- Summary ----- */
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const Podbor = (function () {
|
const Podbor = (function () {
|
||||||
const STORAGE_KEY = "zov-podbor-v1";
|
const STORAGE_KEY = "zov-podbor-v2";
|
||||||
const STEPS = ["intro", "categories", "context", "infra", "scenario", "brands", "summary"];
|
const STEPS = ["intro", "categories", "pricing", "infra", "priorities", "brands", "summary"];
|
||||||
|
|
||||||
let state = loadState();
|
let state = loadState();
|
||||||
let root = null;
|
let root = null;
|
||||||
@ -23,13 +23,11 @@ const Podbor = (function () {
|
|||||||
client_name: "",
|
client_name: "",
|
||||||
client_phone: "",
|
client_phone: "",
|
||||||
address: "",
|
address: "",
|
||||||
budget_total: "",
|
|
||||||
categories: [], // ['fridge','hob',...]
|
categories: [], // ['fridge','hob',...]
|
||||||
niches: {}, // { fridge:{w,h,d}, hob:{w,d}, oven:{w,h,d}, dw:{w,h,d} }
|
price_ranges: {}, // { fridge: { from: 50000, to: 120000 }, ... }
|
||||||
budget_by_cat: {},// { fridge:80000, hob:50000, ... }
|
|
||||||
infra: { stove: "", vent: "" },
|
infra: { stove: "", vent: "" },
|
||||||
scenario: { family: "", cooking: "", techniques: [], guests: "" },
|
priorities: [], // ['balance','reviews',...]
|
||||||
brands: {}, // { fridge: {Bosch:'preferred', Liebherr:'preferred'}, ... }
|
brands: {}, // { fridge: {Bosch:'preferred',...}, ... }
|
||||||
notes: "",
|
notes: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -72,9 +70,9 @@ const Podbor = (function () {
|
|||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case "intro": screen.appendChild(renderIntro()); break;
|
case "intro": screen.appendChild(renderIntro()); break;
|
||||||
case "categories": screen.appendChild(renderCategories()); break;
|
case "categories": screen.appendChild(renderCategories()); break;
|
||||||
case "context": screen.appendChild(renderContext()); break;
|
case "pricing": screen.appendChild(renderPricing()); break;
|
||||||
case "infra": screen.appendChild(renderInfra()); break;
|
case "infra": screen.appendChild(renderInfra()); break;
|
||||||
case "scenario": screen.appendChild(renderScenario()); break;
|
case "priorities": screen.appendChild(renderPriorities()); break;
|
||||||
case "brands": screen.appendChild(renderBrands()); break;
|
case "brands": screen.appendChild(renderBrands()); break;
|
||||||
case "summary": screen.appendChild(renderSummary()); break;
|
case "summary": screen.appendChild(renderSummary()); break;
|
||||||
}
|
}
|
||||||
@ -107,7 +105,7 @@ const Podbor = (function () {
|
|||||||
const idx = STEPS.indexOf(currentStep);
|
const idx = STEPS.indexOf(currentStep);
|
||||||
const total = STEPS.length;
|
const total = STEPS.length;
|
||||||
const pct = Math.round(((idx + 1) / total) * 100);
|
const pct = Math.round(((idx + 1) / total) * 100);
|
||||||
const labels = ["Старт", "Категории", "Контекст", "Инфра", "Сценарий", "Бренды", "Подбор"];
|
const labels = ["Старт", "Категории", "Цена", "Инфра", "Приоритеты", "Бренды", "Подбор"];
|
||||||
return el(`
|
return el(`
|
||||||
<div class="podbor-progress">
|
<div class="podbor-progress">
|
||||||
<div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div>
|
<div class="podbor-progress-bar"><div class="bar" style="width:${pct}%"></div></div>
|
||||||
@ -124,7 +122,7 @@ const Podbor = (function () {
|
|||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2>
|
<h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2>
|
||||||
<p class="lede">7 коротких шагов. Указываем категории, бюджет, инфраструктуру и предпочтения. Дальше AI сам соберёт предложение.</p>
|
<p class="lede">7 коротких шагов. Категории, ценовой коридор, инфраструктура и предпочтения. AI соберёт предложение.</p>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@ -132,15 +130,11 @@ const Podbor = (function () {
|
|||||||
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
|
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row two-col">
|
<div class="form-row">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field-label">Телефон</span>
|
<span class="field-label">Телефон</span>
|
||||||
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ...">
|
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ...">
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<div class="podbor-cta-row">
|
<div class="podbor-cta-row">
|
||||||
@ -189,86 +183,86 @@ const Podbor = (function () {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== Step: context (ниши + бюджет по категориям) ===================== */
|
/* ===================== Step: pricing (ценовой коридор по категориям) ===================== */
|
||||||
|
|
||||||
function renderContext() {
|
function renderPricing() {
|
||||||
const builtinCats = ["fridge", "hob", "oven", "dw"]; // встройка
|
if (!state.categories.length) {
|
||||||
const niches = builtinCats.filter(c => state.categories.includes(c)).map(c => {
|
return el(`
|
||||||
|
<section class="podbor-step">
|
||||||
|
<div class="empty">Сначала выберите категории.</div>
|
||||||
|
<div class="podbor-cta-row">
|
||||||
|
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
// Подсчёт суммы коридоров
|
||||||
|
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 cat = PODBOR_CATEGORIES.find(x => x.key === c);
|
||||||
const n = state.niches[c] || {};
|
const r = state.price_ranges[c] || {};
|
||||||
return `
|
return `
|
||||||
<div class="niche-row">
|
<div class="price-row">
|
||||||
<div class="niche-label">${cat.label}</div>
|
<div class="price-label">${cat.label}</div>
|
||||||
<div class="niche-inputs">
|
<div class="price-inputs">
|
||||||
<input type="number" data-niche="${c}.w" value="${n.w || ""}" placeholder="Ш">
|
<input type="number" inputmode="numeric" data-price="${c}.from" value="${r.from || ""}" placeholder="от">
|
||||||
<input type="number" data-niche="${c}.h" value="${n.h || ""}" placeholder="В">
|
<span class="dash">—</span>
|
||||||
<input type="number" data-niche="${c}.d" value="${n.d || ""}" placeholder="Г">
|
<input type="number" inputmode="numeric" data-price="${c}.to" value="${r.to || ""}" placeholder="до">
|
||||||
|
<span class="rub">₽</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
const budgets = state.categories.map(c => {
|
const totalLine = (totalFrom || totalTo)
|
||||||
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
|
? `<div class="price-total">Итого: <strong>${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽</strong></div>`
|
||||||
const v = state.budget_by_cat[c] || "";
|
: `<div class="price-total muted">Сумма посчитается автоматически</div>`;
|
||||||
return `
|
|
||||||
<label class="field-inline">
|
|
||||||
<span>${cat.label}</span>
|
|
||||||
<input type="number" data-budget="${c}" value="${v}" placeholder="₽">
|
|
||||||
</label>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Размеры<br><span class="accent">и бюджет</span></h2>
|
<h2 class="display-title">Ценовой<br><span class="accent">коридор</span></h2>
|
||||||
<p class="lede">Если планируется встройка — укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.</p>
|
<p class="lede">«От — До» по каждой категории. AI подберёт варианты, которые попадают в коридор и совокупно укладываются в общий бюджет клиента.</p>
|
||||||
|
|
||||||
${niches ? `
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-head">Ниши под встройку, мм</div>
|
<div class="block-head">По категориям, ₽</div>
|
||||||
<div class="niche-list">${niches}</div>
|
<div class="price-list">${rows}</div>
|
||||||
|
${totalLine}
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
|
||||||
|
|
||||||
${budgets ? `
|
|
||||||
<div class="block">
|
|
||||||
<div class="block-head">Бюджет по категориям, ₽</div>
|
|
||||||
<div class="budget-list">${budgets}</div>
|
|
||||||
</div>
|
|
||||||
` : ""}
|
|
||||||
|
|
||||||
<div class="podbor-cta-row">
|
<div class="podbor-cta-row">
|
||||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||||
<button class="btn-primary" data-go="infra">Дальше</button>
|
<button class="btn-primary" data-go="infra">Дальше</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`);
|
`);
|
||||||
node.querySelectorAll("[data-niche]").forEach(inp => {
|
node.querySelectorAll("[data-price]").forEach(inp => {
|
||||||
inp.addEventListener("input", e => {
|
inp.addEventListener("input", e => {
|
||||||
const [cat, dim] = e.target.dataset.niche.split(".");
|
const [cat, key] = e.target.dataset.price.split(".");
|
||||||
const next = { ...state.niches, [cat]: { ...(state.niches[cat] || {}), [dim]: e.target.value } };
|
const next = { ...state.price_ranges, [cat]: { ...(state.price_ranges[cat] || {}), [key]: e.target.value } };
|
||||||
update({ niches: next });
|
update({ price_ranges: next });
|
||||||
});
|
render();
|
||||||
});
|
|
||||||
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);
|
bindNav(node);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRub(n) {
|
||||||
|
if (!n) return "—";
|
||||||
|
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== Step: infra ===================== */
|
/* ===================== Step: infra ===================== */
|
||||||
|
|
||||||
function renderInfra() {
|
function renderInfra() {
|
||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
||||||
<p class="lede">Газ или электрика — главный вопрос для варочной. Шахта вентиляции — для вытяжки.</p>
|
<p class="lede">Газ или электрика — определит тип варочной (индукция / стеклокерамика / газ). Подключение вытяжки — нужны ли выводы или угольный фильтр.</p>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-head">Подключение варочной</div>
|
<div class="block-head">Подключение варочной</div>
|
||||||
<div class="opt-list">
|
<div class="opt-list">
|
||||||
@ -278,16 +272,17 @@ const Podbor = (function () {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-head">Вентиляция для вытяжки</div>
|
<div class="block-head">Вытяжка → внутридомовая вентиляция?</div>
|
||||||
<div class="opt-list">
|
<div class="opt-list">
|
||||||
${PODBOR_INFRA.vent.map(o => `
|
${PODBOR_INFRA.vent.map(o => `
|
||||||
<button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button>
|
<button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button>
|
||||||
`).join("")}
|
`).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint">Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="podbor-cta-row">
|
<div class="podbor-cta-row">
|
||||||
<button class="btn-secondary" data-go="context">Назад</button>
|
<button class="btn-secondary" data-go="pricing">Назад</button>
|
||||||
<button class="btn-primary" data-go="scenario">Дальше</button>
|
<button class="btn-primary" data-go="priorities">Дальше</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`);
|
`);
|
||||||
@ -301,60 +296,34 @@ const Podbor = (function () {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== Step: scenario ===================== */
|
/* ===================== Step: priorities (что важно при выборе) ===================== */
|
||||||
|
|
||||||
function renderScenario() {
|
function renderPriorities() {
|
||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Сценарий<br><span class="accent">использования</span></h2>
|
<h2 class="display-title">Что важно<br><span class="accent">при выборе?</span></h2>
|
||||||
<p class="lede">Семья с детьми готовит иначе, чем пара. AI учтёт это в подборе.</p>
|
<p class="lede">Бюджет уже задал коридор. Здесь — что AI должен использовать как тай-брейк, когда варианты примерно равны по цене.</p>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-head">Состав семьи</div>
|
<div class="block-head">Приоритеты</div>
|
||||||
<div class="opt-list">
|
<div class="opt-list">
|
||||||
${PODBOR_FAMILY.map(o => `
|
${PODBOR_PRIORITIES.map(o => `
|
||||||
<button class="opt${state.scenario.family === o.key ? " on" : ""}" data-scenario="family" data-val="${o.key}">${o.label}</button>
|
<button class="opt${(state.priorities || []).includes(o.key) ? " on" : ""}" data-pri="${o.key}">${o.label}</button>
|
||||||
`).join("")}
|
`).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint">Можно несколько · в порядке выбора</div>
|
||||||
</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">
|
<div class="podbor-cta-row">
|
||||||
<button class="btn-secondary" data-go="infra">Назад</button>
|
<button class="btn-secondary" data-go="infra">Назад</button>
|
||||||
<button class="btn-primary" data-go="brands">Дальше</button>
|
<button class="btn-primary" data-go="brands">Дальше</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`);
|
`);
|
||||||
node.querySelectorAll("[data-scenario]").forEach(b => {
|
node.querySelectorAll("[data-pri]").forEach(b => {
|
||||||
b.addEventListener("click", () => {
|
b.addEventListener("click", () => {
|
||||||
update({ scenario: { ...state.scenario, [b.dataset.scenario]: b.dataset.val } });
|
const cur = state.priorities || [];
|
||||||
render();
|
const key = b.dataset.pri;
|
||||||
});
|
|
||||||
});
|
|
||||||
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];
|
const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
|
||||||
update({ scenario: { ...state.scenario, techniques: next } });
|
update({ priorities: next });
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -372,22 +341,20 @@ const Podbor = (function () {
|
|||||||
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
|
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
|
||||||
const brands = PODBOR_BRANDS[catKey] || { premium: [], middle: [], budget: [] };
|
const brands = PODBOR_BRANDS[catKey] || { premium: [], middle: [], budget: [] };
|
||||||
const catState = state.brands[catKey] || {};
|
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>
|
const tierGroup = (tier) => `
|
||||||
<div class="brand-chips">
|
<div class="brand-chips brand-tier-${tier}">
|
||||||
${(brands[tier] || []).map(b => {
|
${(brands[tier] || []).map(b => {
|
||||||
const status = catState[b] || "none";
|
const status = catState[b] || "none";
|
||||||
return `<button class="chip status-${status}" data-cat="${catKey}" data-brand="${b}">${b}</button>`;
|
return `<button class="chip tier-${tier} status-${status}" data-cat="${catKey}" data-brand="${b}" data-tier="${tier}">${b}</button>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
return `
|
return `
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="block-head">${cat.label}</div>
|
<div class="block-head">${cat.label}</div>
|
||||||
<div class="hint">★ Тап — предпочтительно · ✓ Двойной — допустимо · — Третий — снять</div>
|
${tierGroup("premium")}${tierGroup("middle")}${tierGroup("budget")}
|
||||||
${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
@ -395,10 +362,10 @@ const Podbor = (function () {
|
|||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
|
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
|
||||||
<p class="lede">Какие марки уважаете, какие — допустимы. AI сначала пробует preferred.</p>
|
<p class="lede">Тап — ★ предпочтительно. Дабл — ✓ допустимо. Третий — снять. AI сначала пробует ★, потом ✓.</p>
|
||||||
${blocks}
|
${blocks}
|
||||||
<div class="podbor-cta-row">
|
<div class="podbor-cta-row">
|
||||||
<button class="btn-secondary" data-go="scenario">Назад</button>
|
<button class="btn-secondary" data-go="priorities">Назад</button>
|
||||||
<button class="btn-primary" data-go="summary">Дальше</button>
|
<button class="btn-primary" data-go="summary">Дальше</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -422,17 +389,30 @@ const Podbor = (function () {
|
|||||||
/* ===================== Step: summary + submit ===================== */
|
/* ===================== Step: summary + submit ===================== */
|
||||||
|
|
||||||
function renderSummary() {
|
function renderSummary() {
|
||||||
|
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)
|
||||||
|
.filter(Boolean).join(" · ");
|
||||||
|
|
||||||
const node = el(`
|
const node = el(`
|
||||||
<section class="podbor-step">
|
<section class="podbor-step">
|
||||||
<h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2>
|
<h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2>
|
||||||
<p class="lede">Проверьте и отправьте — AI вернёт предложение в чат с ботом.</p>
|
<p class="lede">Проверьте и отправьте — AI вернёт предложение в чат с ботом.</p>
|
||||||
<div class="block summary-block">
|
<div class="block summary-block">
|
||||||
<div class="kv"><span>Клиент</span><strong>${state.client_name || "—"}</strong></div>
|
<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>${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>${totalRange}</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 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>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
<link rel="stylesheet" href="assets/styles.css?v=20260509l">
|
<link rel="stylesheet" href="assets/styles.css?v=20260509m">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260509l">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260509m">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app">
|
<main id="app">
|
||||||
@ -21,9 +21,9 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="assets/icons.js?v=20260509l"></script>
|
<script src="assets/icons.js?v=20260509m"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260509l"></script>
|
<script src="assets/podbor.config.js?v=20260509m"></script>
|
||||||
<script src="assets/podbor.js?v=20260509l"></script>
|
<script src="assets/podbor.js?v=20260509m"></script>
|
||||||
<script src="assets/app.js?v=20260509l"></script>
|
<script src="assets/app.js?v=20260509m"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user