From 129046de0774d078c0ddcdd47e5929710a716227 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sat, 9 May 2026 15:10:23 +0300 Subject: [PATCH] feat(podbor): drop niches; price-range from-to per cat; ventilation Y/N; priorities multi-select; brand tiers as color (no labels) --- miniapp/assets/podbor.config.js | 33 ++--- miniapp/assets/podbor.css | 139 ++++++++++++++++--- miniapp/assets/podbor.js | 228 +++++++++++++++----------------- miniapp/index.html | 12 +- 4 files changed, 241 insertions(+), 171 deletions(-) diff --git a/miniapp/assets/podbor.config.js b/miniapp/assets/podbor.config.js index 63bcc54..e7a867f 100644 --- a/miniapp/assets/podbor.config.js +++ b/miniapp/assets/podbor.config.js @@ -19,19 +19,6 @@ const PODBOR_BUDGET_TIERS = [ { 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 = { stove: [ { key: "induction", label: "Индукция / 380 В" }, @@ -40,19 +27,19 @@ const PODBOR_INFRA = { { key: "any", label: "Не знаю / любой" }, ], vent: [ - { key: "shaft", label: "Шахта вентиляции есть" }, - { key: "no_shaft", label: "Только рециркуляция" }, - { key: "unknown", label: "Не знаю" }, + { key: "yes", label: "Да — есть выводы в вентиляцию" }, + { key: "no", label: "Нет — рециркуляция с угольным фильтром" }, + { key: "unknown", label: "Не знаю — менеджер уточнит" }, ], }; -const PODBOR_TECHNIQUES = [ - { key: "bake", label: "Выпечка" }, - { key: "steam", label: "На пару" }, - { key: "grill", label: "Гриль" }, - { key: "wok", label: "Wok / стир-фрай" }, - { key: "low_t", label: "Низкотемпературное" }, - { key: "smart", label: "Умные режимы / Smart" }, +const PODBOR_PRIORITIES = [ + { key: "balance", label: "Цена / качество" }, + { key: "reviews", label: "Отзывы" }, + { key: "popular", label: "Популярность бренда" }, + { key: "design", label: "Дизайн и цвет" }, + { key: "tech", label: "Технологичность" }, + { key: "service", label: "Сервис и гарантия" }, ]; /* Бренды для каждой категории — для чипов с тирами. diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 0110765..9449d84 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -258,6 +258,72 @@ 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 ----- */ .opt-list { display: flex; flex-wrap: wrap; gap: 6px; } @@ -283,49 +349,86 @@ } /* ----- Brand chips ----- */ -.tier-row { display: flex; flex-direction: column; gap: 6px; padding: 8px 0; border-top: 1px solid var(--line); } -.tier-row:first-child { border-top: none; padding-top: 0; } +/* Тиры (Premium/Middle/Budget) различаются цветом, без явных текстовых ярлыков. + Внутренне храним tier для аналитики «температуры» клиента. */ -.tier-label { - font-family: var(--font-mono); - font-size: 9.5px; - font-weight: 500; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--muted); +.brand-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 6px 0; + border-top: 1px dashed var(--line); } -.brand-chips { display: flex; flex-wrap: wrap; gap: 6px; } +.brand-chips:first-of-type { + border-top: none; + padding-top: 0; +} .chip { font-family: var(--font-ui); font-size: 12.5px; font-weight: 500; - padding: 6px 10px; + padding: 6px 11px; border-radius: var(--r-tag); - border: 1px solid var(--line-strong); - background: var(--paper); - color: var(--muted); + border: 1px solid; cursor: pointer; transition: all 0.12s; 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); border-color: var(--accent-2); 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: "★ "; } +/* Состояние acceptable — обводка цветом тира, прозрачная заливка */ .chip.status-acceptable { 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: "✓ "; } /* ----- Summary ----- */ diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index 2a9daa9..b325cf4 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -3,8 +3,8 @@ ============================================================ */ const Podbor = (function () { - const STORAGE_KEY = "zov-podbor-v1"; - const STEPS = ["intro", "categories", "context", "infra", "scenario", "brands", "summary"]; + const STORAGE_KEY = "zov-podbor-v2"; + const STEPS = ["intro", "categories", "pricing", "infra", "priorities", "brands", "summary"]; let state = loadState(); let root = null; @@ -23,13 +23,11 @@ const Podbor = (function () { 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, ... } + categories: [], // ['fridge','hob',...] + price_ranges: {}, // { fridge: { from: 50000, to: 120000 }, ... } infra: { stove: "", vent: "" }, - scenario: { family: "", cooking: "", techniques: [], guests: "" }, - brands: {}, // { fridge: {Bosch:'preferred', Liebherr:'preferred'}, ... } + priorities: [], // ['balance','reviews',...] + brands: {}, // { fridge: {Bosch:'preferred',...}, ... } notes: "", }; } @@ -72,9 +70,9 @@ const Podbor = (function () { switch (currentStep) { case "intro": screen.appendChild(renderIntro()); 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 "scenario": screen.appendChild(renderScenario()); break; + case "priorities": screen.appendChild(renderPriorities()); break; case "brands": screen.appendChild(renderBrands()); break; case "summary": screen.appendChild(renderSummary()); break; } @@ -107,7 +105,7 @@ const Podbor = (function () { const idx = STEPS.indexOf(currentStep); const total = STEPS.length; const pct = Math.round(((idx + 1) / total) * 100); - const labels = ["Старт", "Категории", "Контекст", "Инфра", "Сценарий", "Бренды", "Подбор"]; + const labels = ["Старт", "Категории", "Цена", "Инфра", "Приоритеты", "Бренды", "Подбор"]; return el(`
@@ -124,7 +122,7 @@ const Podbor = (function () { const node = el(`

Подбор техники
для клиента

-

7 коротких шагов. Указываем категории, бюджет, инфраструктуру и предпочтения. Дальше AI сам соберёт предложение.

+

7 коротких шагов. Категории, ценовой коридор, инфраструктура и предпочтения. AI соберёт предложение.

-
+
-
@@ -189,86 +183,86 @@ const Podbor = (function () { return node; } - /* ===================== Step: context (ниши + бюджет по категориям) ===================== */ + /* ===================== Step: pricing (ценовой коридор по категориям) ===================== */ - function renderContext() { - const builtinCats = ["fridge", "hob", "oven", "dw"]; // встройка - const niches = builtinCats.filter(c => state.categories.includes(c)).map(c => { + function renderPricing() { + if (!state.categories.length) { + return el(` +
+
Сначала выберите категории.
+
+ +
+
+ `); + } + // Подсчёт суммы коридоров + 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 n = state.niches[c] || {}; + const r = state.price_ranges[c] || {}; return ` -
-
${cat.label}
-
- - - +
+
${cat.label}
+
+ + + +
`; }).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 totalLine = (totalFrom || totalTo) + ? `
Итого: ${formatRub(totalFrom)} — ${formatRub(totalTo)} ₽
` + : `
Сумма посчитается автоматически
`; const node = el(`
-

Размеры
и бюджет

-

Если планируется встройка — укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.

- - ${niches ? ` -
-
Ниши под встройку, мм
-
${niches}
-
- ` : ""} - - ${budgets ? ` -
-
Бюджет по категориям, ₽
-
${budgets}
-
- ` : ""} - +

Ценовой
коридор

+

«От — До» по каждой категории. AI подберёт варианты, которые попадают в коридор и совокупно укладываются в общий бюджет клиента.

+
+
По категориям, ₽
+
${rows}
+ ${totalLine} +
`); - node.querySelectorAll("[data-niche]").forEach(inp => { + node.querySelectorAll("[data-price]").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 }); + 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(); }); }); bindNav(node); return node; } + function formatRub(n) { + if (!n) return "—"; + return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); + } + /* ===================== Step: infra ===================== */ function renderInfra() { const node = el(`

Инфраструктура
кухни

-

Газ или электрика — главный вопрос для варочной. Шахта вентиляции — для вытяжки.

+

Газ или электрика — определит тип варочной (индукция / стеклокерамика / газ). Подключение вытяжки — нужны ли выводы или угольный фильтр.

Подключение варочной
@@ -278,16 +272,17 @@ const Podbor = (function () {
-
Вентиляция для вытяжки
+
Вытяжка → внутридомовая вентиляция?
${PODBOR_INFRA.vent.map(o => ` `).join("")}
+
Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.
- - + +
`); @@ -301,60 +296,34 @@ const Podbor = (function () { return node; } - /* ===================== Step: scenario ===================== */ + /* ===================== Step: priorities (что важно при выборе) ===================== */ - function renderScenario() { + function renderPriorities() { const node = el(`
-

Сценарий
использования

-

Семья с детьми готовит иначе, чем пара. AI учтёт это в подборе.

- +

Что важно
при выборе?

+

Бюджет уже задал коридор. Здесь — что AI должен использовать как тай-брейк, когда варианты примерно равны по цене.

-
Состав семьи
+
Приоритеты
- ${PODBOR_FAMILY.map(o => ` - + ${PODBOR_PRIORITIES.map(o => ` + `).join("")}
+
Можно несколько · в порядке выбора
- -
-
Частота готовки
-
- ${PODBOR_COOKING.map(o => ` - - `).join("")} -
-
- -
-
Любимые техники приготовления
-
- ${PODBOR_TECHNIQUES.map(o => ` - - `).join("")} -
-
Можно несколько
-
-
`); - node.querySelectorAll("[data-scenario]").forEach(b => { + node.querySelectorAll("[data-pri]").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 cur = state.priorities || []; + const key = b.dataset.pri; const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key]; - update({ scenario: { ...state.scenario, techniques: next } }); + update({ priorities: next }); render(); }); }); @@ -372,22 +341,20 @@ const Podbor = (function () { 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("")} -
+ // Тиры остаются в данных (для аналитики «температуры» клиента), + // но визуально просто разный цветовой оттенок чипа — без явного ярлыка. + const tierGroup = (tier) => ` +
+ ${(brands[tier] || []).map(b => { + const status = catState[b] || "none"; + return ``; + }).join("")}
`; return `
${cat.label}
-
★ Тап — предпочтительно · ✓ Двойной — допустимо · — Третий — снять
- ${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")} + ${tierGroup("premium")}${tierGroup("middle")}${tierGroup("budget")}
`; }).join(""); @@ -395,10 +362,10 @@ const Podbor = (function () { const node = el(`

Бренды
по категориям

-

Какие марки уважаете, какие — допустимы. AI сначала пробует preferred.

+

Тап — ★ предпочтительно. Дабл — ✓ допустимо. Третий — снять. AI сначала пробует ★, потом ✓.

${blocks}
- +
@@ -422,17 +389,30 @@ const Podbor = (function () { /* ===================== Step: summary + submit ===================== */ 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(`

Готово
к подбору

Проверьте и отправьте — 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 || "—"}
+
Ценовой коридор${totalRange}
Подключение${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || "—"}
+
Вентиляция${PODBOR_INFRA.vent.find(f => f.key === state.infra.vent)?.label || "—"}
+
Приоритеты${priorityLabels || "—"}
- - - - + + + +