From 80580db446e3e4eb06748f2d95d7b13eb83746cd Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 11 May 2026 14:25:25 +0300 Subject: [PATCH] miniapp: 4 UX fixes from user feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. PHONE NORMALIZATION - On blur (or before submit): '9001234567' -> '+7 900 123-45-67' - Handles 8XXX, 7XXX, +7XXX, 10-digit mobile prefixes - Leaves untouched if not Russian-looking number 2. BRAND LIST FOR RF 2026 - PODBOR_SINGLE_BRAND_OPTIONS updated with realistic 2026 brands - Promoted: Haier, Korting, Midea, Hisense, Бирюса, Атлант, Pozis, DEXP - Bosch/Siemens marked with ⚠ (parallel-import) - Miele/Liebherr/Smeg also marked ⚠ - PODBOR_BRANDS per-category fully refreshed 3. BUDGET ADAPTIVE HINTS - Hints now scale by selected categories share of full kitchen - Just fridge picked → 'Средний' shows ~88-175 тыс instead of 350-700к - Full 8 categories → original 350-700к - PODBOR_BUDGET_SHARES + PODBOR_BUDGET_RANGES constants 4. INFRA STEP CONDITIONAL - Stove power question only shown if hob category picked - Vent question only shown if hood category picked - If neither → step auto-skips to summary (with brief notice) - Summary 'Назад' button respects skip — goes to strategy if needed --- miniapp/assets/podbor.config.js | 118 ++++++++++++++++++------------ miniapp/assets/podbor.js | 122 ++++++++++++++++++++++++++------ miniapp/index.html | 14 ++-- 3 files changed, 181 insertions(+), 73 deletions(-) diff --git a/miniapp/assets/podbor.config.js b/miniapp/assets/podbor.config.js index 2f2a440..5fc9228 100644 --- a/miniapp/assets/podbor.config.js +++ b/miniapp/assets/podbor.config.js @@ -50,30 +50,56 @@ const PODBOR_BRAND_STRATEGY = [ { key: "different", label: "Разные марки по категориям", hint: "соберём оптимальный микс" }, ]; -/* Бренды, у которых есть полная линейка кухонной техники (для single-mode) */ +/* Бренды, у которых есть полная линейка кухонной техники, реально доступные в РФ (2026). + tier: premium / middle / budget · note: "available" | "parallel" (параллельный импорт). */ 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 }, + // Премиум — официально или через параллельный импорт + { key: "miele", label: "Miele", tier: "premium", note: "parallel" }, + { key: "asko", label: "Asko", tier: "premium", note: "available" }, + { key: "smeg", label: "Smeg", tier: "premium", note: "parallel" }, + { key: "gorenje", label: "Gorenje", tier: "premium", note: "available" }, + + // Средний — реально работающие бренды + { key: "haier", label: "Haier", tier: "middle", note: "available" }, + { key: "samsung", label: "Samsung", tier: "middle", note: "available" }, + { key: "lg", label: "LG", tier: "middle", note: "available" }, + { key: "korting", label: "Körting", tier: "middle", note: "available" }, + { key: "midea", label: "Midea", tier: "middle", note: "available" }, + { key: "bosch", label: "Bosch ⚠", tier: "middle", note: "parallel" }, + { key: "siemens", label: "Siemens ⚠", tier: "middle", note: "parallel" }, + + // Бюджет — российские/китайские + { key: "biryusa", label: "Бирюса", tier: "budget", note: "available" }, + { key: "atlant", label: "Атлант", tier: "budget", note: "available" }, + { key: "pozis", label: "Pozis", tier: "budget", note: "available" }, + { key: "hisense", label: "Hisense", tier: "budget", note: "available" }, + { key: "hansa", label: "Hansa", tier: "budget", note: "available" }, + { key: "dexp", label: "DEXP", tier: "budget", note: "available" }, + + { key: "ai_pick", label: "Пусть AI выберет под бюджет", recommended: true }, ]; +/* Доля бюджета каждой категории от полного комплекта (для адаптивных вилок). */ +const PODBOR_BUDGET_SHARES = { + fridge: 25, hob: 12, oven: 15, dw: 10, + hood: 8, microwave: 5, coffee: 15, washer: 10, +}; + +/* Базовые вилки для ПОЛНОГО комплекта 8 категорий (в тыс. ₽). + Адаптируются по выбранным категориям через PODBOR_BUDGET_SHARES. */ +const PODBOR_BUDGET_RANGES = { + luxe: { from: 1500, to: 3000 }, // от 1.5М + premium: { from: 700, to: 1500 }, + middle: { from: 350, to: 700 }, + budget: { from: 100, to: 350 }, +}; + 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: "ввести от-до по категориям" }, + { key: "luxe", label: "Люкс", desc: "лучшее без оглядки на цену" }, + { key: "premium", label: "Премиум", desc: "топовые модели · все опции" }, + { key: "middle", label: "Средний", desc: "оптимальный баланс · цена/функции", recommended: true }, + { key: "budget", label: "Бюджет", desc: "только нужное" }, + { key: "exact", label: "Точные цифры", desc: "вилки от-до по каждой категории" }, ]; const PODBOR_PICK_STRATEGIES = [ @@ -635,47 +661,47 @@ const PODBOR_PARAMS = { }, }; -/* Бренды для каждой категории — для чипов с тирами. - Сокращённый набор; полный список можно расширить из исходного HTML. */ +/* Бренды по категориям (актуально на 2026, РФ). + ⚠ — параллельный импорт, остальные — официально доступны. */ const PODBOR_BRANDS = { fridge: { - premium: ["Liebherr", "Miele", "Sub-Zero", "V-ZUG"], - middle: ["Bosch", "Siemens", "Samsung", "LG"], - budget: ["Indesit", "Beko", "Hotpoint"], + premium: ["Miele ⚠", "Liebherr ⚠", "Asko", "Gorenje"], + middle: ["Haier", "Samsung", "LG", "Korting", "Bosch ⚠", "Siemens ⚠"], + budget: ["Бирюса", "Атлант", "Pozis", "Hisense", "Indesit", "Hansa"], }, hob: { - premium: ["Miele", "Gaggenau", "AEG"], - middle: ["Bosch", "Siemens", "Electrolux", "Hansa"], - budget: ["Hotpoint", "Beko", "Indesit"], + premium: ["Miele ⚠", "Asko", "Gorenje", "Smeg ⚠"], + middle: ["Korting", "Haier", "Midea", "Bosch ⚠", "Siemens ⚠"], + budget: ["Hansa", "Hisense", "DEXP", "Дарина"], }, oven: { - premium: ["Miele", "Gaggenau", "Neff"], - middle: ["Bosch", "Siemens", "Electrolux", "AEG"], - budget: ["Hansa", "Beko", "Hotpoint"], + premium: ["Miele ⚠", "Asko", "Gorenje", "Smeg ⚠"], + middle: ["Korting", "Haier", "Midea", "Samsung", "Bosch ⚠"], + budget: ["Hansa", "Hisense", "DEXP", "Дарина"], }, dw: { - premium: ["Miele", "Asko", "V-ZUG"], - middle: ["Bosch", "Siemens", "Electrolux"], - budget: ["Hansa", "Beko", "Indesit"], + premium: ["Miele ⚠", "Asko", "Gorenje"], + middle: ["Haier", "Midea", "Korting", "Bosch ⚠"], + budget: ["Hansa", "Hisense", "Indesit"], }, hood: { - premium: ["Miele", "Falmec", "Faber"], - middle: ["Bosch", "Siemens", "Elica"], - budget: ["Hansa", "Hotpoint", "Maunfeld"], + premium: ["Miele ⚠", "Falmec ⚠", "Faber ⚠", "Gorenje"], + middle: ["Korting", "Maunfeld", "Elikor", "Haier"], + budget: ["Hansa", "Hisense", "DEXP", "Krona"], }, microwave: { - premium: ["Miele", "Neff"], - middle: ["Bosch", "Siemens", "Samsung", "LG"], - budget: ["Whirlpool", "Hansa", "Beko"], + premium: ["Miele ⚠", "Asko"], + middle: ["Samsung", "LG", "Haier", "Midea", "Bosch ⚠"], + budget: ["Hansa", "Hisense", "DEXP", "Polaris"], }, coffee: { - premium: ["Miele", "Jura", "De'Longhi PrimaDonna"], - middle: ["De'Longhi", "Saeco", "Bosch"], - budget: ["Krups", "Philips"], + premium: ["Miele ⚠", "Jura ⚠", "Saeco ⚠"], + middle: ["De'Longhi ⚠", "Philips ⚠", "Polaris", "Bork ⚠"], + budget: ["Polaris", "Redmond", "Kitfort"], }, washer: { - premium: ["Miele", "Asko", "V-ZUG"], - middle: ["Bosch", "Siemens", "Samsung", "LG"], - budget: ["Indesit", "Hotpoint", "Beko"], + premium: ["Miele ⚠", "Asko", "Gorenje"], + middle: ["Haier", "Samsung", "LG", "Korting", "Bosch ⚠"], + budget: ["Атлант", "Indesit", "Hansa", "Hisense"], }, }; diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index 07da6dd..714373e 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -892,8 +892,26 @@ const Podbor = (function () { function renderBudget() { const bp = state.budget_preset || ""; + + // Считаем суммарную долю выбранных категорий от полного комплекта + const share = (state.categories || []).reduce( + (s, c) => s + (PODBOR_BUDGET_SHARES[c] || 0), 0 + ) / 100; + const shareSafe = share > 0 ? share : 1; + + // Подмешиваем hint с реальной вилкой для выбранных категорий + const presetsWithRange = PODBOR_BUDGET_PRESETS.map(o => { + if (o.key === "exact") return { ...o, hint: o.desc }; + const r = PODBOR_BUDGET_RANGES[o.key]; + if (!r) return { ...o, hint: o.desc }; + const from = Math.round(r.from * shareSafe); + const to = Math.round(r.to * shareSafe); + const label = from >= 1000 ? `${(from/1000).toFixed(1)}–${(to/1000).toFixed(1)}М ₽` : `${from}–${to} тыс ₽`; + return { ...o, hint: `${label} · ${o.desc}` }; + }); + const presetGrid = renderPinCards( - PODBOR_BUDGET_PRESETS, + presetsWithRange, o => (bp === o.key ? "on" : ""), key => { update({ budget_preset: key }); render(); } ); @@ -1010,27 +1028,58 @@ const Podbor = (function () { /* ===================== Step: infra ===================== */ function renderInfra() { + const cats = state.categories || []; + const askStove = cats.includes("hob"); + const askVent = cats.includes("hood"); + + // Если ни одна из релевантных категорий не выбрана — пропускаем шаг + if (!askStove && !askVent) { + // Автопереход на summary через микропаузу (чтобы пользователь увидел) + setTimeout(() => { go("summary"); }, 50); + return el(` +
+

+ Инфраструктурные вопросы для выбранных категорий не требуются — переходим к итогу... +

+
+ `); + } + + const stoveBlock = askStove ? ` +
+
Подключение варочной
+
+ ${PODBOR_INFRA.stove.map(o => ` + + `).join("")} +
+
+ ` : ""; + + const ventBlock = askVent ? ` +
+
Вытяжка → внутридомовая вентиляция?
+
+ ${PODBOR_INFRA.vent.map(o => ` + + `).join("")} +
+
Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.
+
+ ` : ""; + + const lede = (askStove && askVent) + ? "Газ или электрика — определит тип варочной. Подключение вытяжки — нужны ли выводы или угольный фильтр." + : askStove + ? "Газ или электрика — определит тип варочной (индукция / стеклокерамика / газ)." + : "Подключение вытяжки — нужны ли выводы в вентшахту или угольный фильтр."; + const node = el(`

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

-

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

-
-
Подключение варочной
-
- ${PODBOR_INFRA.stove.map(o => ` - - `).join("")} -
-
-
-
Вытяжка → внутридомовая вентиляция?
-
- ${PODBOR_INFRA.vent.map(o => ` - - `).join("")} -
-
Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.
-
+

${lede}

+ ${stoveBlock} + ${ventBlock}
@@ -1103,7 +1152,7 @@ const Podbor = (function () {
- +
@@ -1112,6 +1161,12 @@ const Podbor = (function () { `); bindInputs(node); bindNav(node); + // Кнопка "Назад" — обходим infra если она авто-пропускается + node.querySelector("#summaryBack").addEventListener("click", () => { + const cats = state.categories || []; + const goTo = (cats.includes("hob") || cats.includes("hood")) ? "infra" : "strategy"; + go(goTo); + }); node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node)); return node; } @@ -1131,6 +1186,11 @@ const Podbor = (function () { } try { + // Финальная нормализация телефона перед отправкой + const normPhone = normalizePhone(state.client_phone || ""); + if (normPhone && normPhone !== state.client_phone) { + update({ client_phone: normPhone }); + } const res = await fetch(`${BACKEND_URL}/api/podbor`, { method: "POST", body: JSON.stringify({ @@ -1335,9 +1395,31 @@ const Podbor = (function () { inp.addEventListener("input", e => { update({ [e.target.dataset.bind]: e.target.value }); }); + // Нормализация телефона на blur + if (inp.dataset.bind === "client_phone") { + inp.addEventListener("blur", e => { + const normalized = normalizePhone(e.target.value); + if (normalized && normalized !== e.target.value) { + e.target.value = normalized; + update({ client_phone: normalized }); + } + }); + } }); } + /* Приводим телефон к единому формату +7 XXX XXX-XX-XX. + Принимает: 8XXXXXXXXXX, 7XXXXXXXXXX, +7XXXXXXXXXX, 9XXXXXXXXX (без префикса). */ + function normalizePhone(raw) { + if (!raw) return ""; + const digits = raw.replace(/\D/g, ""); + let d = digits; + if (d.length === 11 && d.startsWith("8")) d = "7" + d.slice(1); + if (d.length === 10 && d.startsWith("9")) d = "7" + d; // мобильный без префикса + if (d.length !== 11 || !d.startsWith("7")) return raw.trim(); // не похоже на РФ-номер — не трогаем + return `+7 ${d.slice(1, 4)} ${d.slice(4, 7)}-${d.slice(7, 9)}-${d.slice(9, 11)}`; + } + function bindNav(node) { node.querySelectorAll("[data-go]").forEach(b => { b.addEventListener("click", () => go(b.dataset.go)); diff --git a/miniapp/index.html b/miniapp/index.html index bdcc46c..3e1f8e5 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + +
@@ -21,10 +21,10 @@
- - - - - + + + + +