From cc28984122ed7784369f9845e48bf94dbcd94559 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sat, 9 May 2026 15:21:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(podbor=20iter2):=20per-category=20detail?= =?UTF-8?q?=20menu=20with=20primary=20params=20+=20accordion=20'=D0=9F?= =?UTF-8?q?=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D0=B5=D0=B5'=20for=20tech?= =?UTF-8?q?=20features;=20updated=20AI=20prompt=20to=20require=20feature?= =?UTF-8?q?=20explanations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Code.gs | 43 ++++-- miniapp/assets/podbor.config.js | 217 ++++++++++++++++++++++++++++ miniapp/assets/podbor.css | 247 ++++++++++++++++++++++++++++++++ miniapp/assets/podbor.js | 203 +++++++++++++++++++++++++- miniapp/index.html | 12 +- 5 files changed, 698 insertions(+), 24 deletions(-) diff --git a/backend/Code.gs b/backend/Code.gs index 0815731..4dc2d1a 100644 --- a/backend/Code.gs +++ b/backend/Code.gs @@ -494,25 +494,44 @@ function synthesizeManagerFromUser(user) { const SYSTEM_PROMPT_PICKER = ( "Ты — эксперт-консультант по подбору кухонной техники для фабрики мебели «ЗОВ».\n" + - "Помогаешь менеджерам салонов быстро согласовать с клиентом комплект техники.\n\n" + - "Принципы:\n" + - "1. Физические ограничения важнее эстетики. Если ниша 600×1850×600 — не предлагай 700×2000×650.\n" + - "2. Уважай бюджет. Лимит в категории — не превышай >10%.\n" + - "3. Уважай предпочтения по брендам: сначала preferred (★), потом alternative (✓).\n" + - "4. Сценарий использования. Семья с детьми = простой UI, защита от детей. Выпечка = пар + конвекция.\n" + - "5. Инфраструктура. Газ исключает индукцию. Нет шахты = только рециркуляция.\n" + - "6. По каждой позиции: модель (линейка), цена, 2-3 преимущества под клиента, 1 предупреждение.\n\n" + + "Помогаешь менеджерам салонов согласовать с клиентом комплект техники.\n\n" + + "Принципы подбора:\n" + + "1. Уважай ценовой коридор. У каждой категории есть `price_ranges.{cat}.from..to` — попадай в него (±5% допустимо).\n" + + "2. Уважай предпочтения по брендам: сначала preferred (★), потом acceptable (✓), потом нейтральные альтернативы.\n" + + "3. Учитывай инфраструктуру: газ исключает индукцию (если клиент не готов менять подключение); нет вентиляции (`vent: no`) = только рециркуляция с угольным фильтром.\n" + + "4. Учитывай приоритеты выбора (`priorities`): «цена/качество» → балансные модели; «отзывы» → проверенные хиты; «дизайн и цвет» → подбирай эстетику; «технологичность» → топовые фичи.\n" + + "5. Если клиент явно отметил features в `per_cat.{cat}.features` — обязательно ставь модели с этими фичами.\n" + + "6. ВАЖНО: каждую тех. фичу в highlights ОБЯЗАТЕЛЬНО объясняй простым языком в скобках. Менеджер и клиент не обязаны знать что такое NoFrost / FlexZone / 4D HotAir.\n\n" + + "Примеры пояснений (используй такой стиль):\n" + + " «NoFrost (не нужно размораживать вручную)»\n" + + " «PowerBoost (форсаж — кипятит за минуту)»\n" + + " «FlexZone (объединяет зоны под большую сковороду)»\n" + + " «Hob2Hood (вытяжка автоматически работает синхронно с варочной)»\n" + + " «4D HotAir (конвекция с 4 сторон — равномерное запекание)»\n" + + " «Термощуп (готовит до точной температуры medium / well-done)»\n" + + " «AquaStop (защита от протечек — машина сама перекроет воду)»\n" + + " «SoftClose (дверца закрывается плавно, без хлопка)»\n" + + " «Инвертор (тише и экономия ~30% электричества)»\n\n" + "Формат ответа — валидный JSON без markdown:\n" + "{\n" + - ' "summary": "...", \n' + - ' "items": [{"category":"fridge","brand":"Bosch","model":"Serie 4 60см","price_rub":79990,' + - '"size_mm":{"w":600,"h":2030,"d":660},"fits_niche":true,"highlights":["NoFrost","инвертор"],' + - '"caveats":"Глубина 660мм","match_score":0.92}],\n' + + ' "summary": "1-2 предложения общего вывода",\n' + + ' "items": [{\n' + + ' "category": "fridge",\n' + + ' "brand": "Bosch",\n' + + ' "model": "Serie 4 KGN39LB35R",\n' + + ' "price_rub": 79990,\n' + + ' "highlights": ["NoFrost (не нужно размораживать вручную)", "Инвертор (тише и экономия ~30%)", "Зона свежести (овощи дольше хрустящие)"],\n' + + ' "caveats": "Глубина 660мм — на 60мм больше стандартной ниши",\n' + + ' "match_score": 0.92,\n' + + ' "tier_signal": "middle"\n' + + ' }],\n' + ' "total_price_rub": 350000,\n' + ' "budget_status": "в_рамках|превышение|значительно_ниже",\n' + + ' "client_temperature": "premium|middle|budget|mixed",\n' + ' "warnings": [],\n' + ' "next_steps": []\n' + "}\n\n" + + "tier_signal — твоя оценка позиционирования модели. client_temperature — общий типаж клиента по сумме выбора (для аналитики).\n" + "Не выдумывай несуществующие артикулы — указывай линейку (Bosch Serie 4 60см)." ); diff --git a/miniapp/assets/podbor.config.js b/miniapp/assets/podbor.config.js index e7a867f..92c815f 100644 --- a/miniapp/assets/podbor.config.js +++ b/miniapp/assets/podbor.config.js @@ -42,6 +42,223 @@ const PODBOR_PRIORITIES = [ { key: "service", label: "Сервис и гарантия" }, ]; +/* Параметры по категориям: Главное (всегда видно) + Подробнее (свёрнуто) */ +const PODBOR_PARAMS = { + fridge: { + primary: [ + { key: "type", label: "Тип", options: [ + { key: "two_chamber", label: "Двухкамерный" }, + { key: "sbs", label: "Side-by-side" }, + { key: "french", label: "French Door" }, + { key: "column", label: "Колонна (встр.)" }, + { key: "combi", label: "Комбинированный" }, + ]}, + { key: "width", label: "Ширина, см", options: [ + { key: "54", label: "54" }, { key: "60", label: "60" }, + { key: "70", label: "70" }, { key: "75", label: "75" }, { key: "91", label: "91" }, + ]}, + { key: "volume", label: "Объём, л", options: [ + { key: "to300", label: "до 300" }, + { key: "300-450", label: "300–450" }, + { key: "450-600", label: "450–600" }, + { key: "600+", label: "600+" }, + ]}, + { key: "color", label: "Цвет", options: [ + { key: "white", label: "Белый" }, + { key: "inox", label: "Нерж. сталь" }, + { key: "black", label: "Чёрный" }, + { key: "anthracite", label: "Антрацит" }, + { key: "builtin", label: "Под фасад" }, + ]}, + ], + features: [ + { key: "nofrost", label: "NoFrost", hint: "не нужно размораживать вручную" }, + { key: "inverter", label: "Инвертор", hint: "тише и экономичнее на ~30%" }, + { key: "freshzone", label: "Зона свежести", hint: "овощи и зелень дольше хрустящие" }, + { key: "silent", label: "≤40 дБ", hint: "почти не слышно ночью" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "управление с телефона" }, + { key: "ice", label: "Лёдогенератор", hint: "автоматически делает кубики" }, + ], + }, + hob: { + primary: [ + { key: "heat", label: "Тип нагрева", options: [ + { key: "induction", label: "Индукция" }, + { key: "hi_light", label: "Hi-Light (стеклокерамика)" }, + { key: "gas", label: "Газ" }, + { key: "domino", label: "Domino (модульная)" }, + ]}, + { key: "width", label: "Ширина, см", options: [ + { key: "30", label: "30" }, { key: "45", label: "45" }, + { key: "60", label: "60" }, { key: "80", label: "80" }, { key: "90", label: "90" }, + ]}, + { key: "zones", label: "Число зон", options: [ + { key: "2", label: "2" }, { key: "3", label: "3" }, + { key: "4", label: "4" }, { key: "5", label: "5" }, + ]}, + { key: "color", label: "Цвет", options: [ + { key: "black", label: "Чёрный" }, { key: "white", label: "Белый" }, + { key: "frameless", label: "Без рамки" }, { key: "inox", label: "Нерж. сталь" }, + ]}, + ], + features: [ + { key: "boost", label: "PowerBoost", hint: "форсаж — кипятит за минуту" }, + { key: "flex", label: "FlexZone", hint: "объединяет зоны под большую сковороду" }, + { key: "hob2hood", label: "Hob2Hood", hint: "вытяжка автоматически следит за варочной" }, + { key: "child_lock", label: "Защита от детей", hint: "блокировка панели" }, + ], + }, + oven: { + primary: [ + { key: "config", label: "Конфигурация", options: [ + { key: "compact_combi", label: "Компакт + СВЧ" }, + { key: "full_60", label: "Полный 60 см" }, + { key: "xl_90", label: "XL 90 см" }, + { key: "two_separate", label: "2 отдельных прибора" }, + ]}, + { key: "color", label: "Цвет", options: [ + { key: "black", label: "Чёрный" }, + { key: "inox", label: "Нерж. сталь" }, + { key: "white", label: "Белый" }, + { key: "blackglass", label: "Чёрное стекло" }, + { key: "anthracite", label: "Антрацит" }, + ]}, + { key: "cleaning", label: "Очистка", options: [ + { key: "hydro", label: "Гидролиз" }, + { key: "pyro", label: "Пиролиз" }, + { key: "eco", label: "Eco / каталитическая" }, + { key: "aqua", label: "Aqua" }, + { key: "std", label: "Стандарт" }, + ]}, + ], + features: [ + { key: "4d", label: "4D HotAir", hint: "конвекция с 4 сторон — равномерное запекание" }, + { key: "steam", label: "Пар", hint: "хлеб с румяной корочкой, мясо без пересушки" }, + { key: "probe", label: "Термощуп", hint: "готовит до точной температуры (medium / well-done)" }, + { key: "autopilot", label: "Автопилот", hint: "выбираешь блюдо — духовка сама ставит режим" }, + { key: "softclose", label: "SoftClose", hint: "дверца закрывается плавно" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "следишь за приготовлением с телефона" }, + ], + }, + dw: { + primary: [ + { key: "width", label: "Ширина, см", options: [ + { key: "45", label: "45" }, { key: "60", label: "60" }, + ]}, + { key: "mount", label: "Монтаж", options: [ + { key: "full_built_in", label: "Полная встройка (под фасад)" }, + { key: "partial", label: "Частичная встройка" }, + { key: "freestanding", label: "Отдельная" }, + ]}, + { key: "settings", label: "Комплектов", options: [ + { key: "8-9", label: "8–9 (для 2–3 человек)" }, + { key: "10-11", label: "10–11 (семья 3–4)" }, + { key: "12-14", label: "12–14 (большая семья)" }, + ]}, + ], + features: [ + { key: "aquastop", label: "AquaStop", hint: "защита от протечек — машина сама перекроет воду" }, + { key: "tray", label: "3-й лоток", hint: "отдельная полка для столовых приборов" }, + { key: "autoopen", label: "AutoOpen", hint: "приоткрывает дверь после мойки — сухая посуда" }, + { key: "silent", label: "≤44 дБ", hint: "можно мыть ночью, не слышно" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "уведомление на телефон когда готово" }, + ], + }, + hood: { + primary: [ + { key: "type", label: "Тип", options: [ + { key: "inclined", label: "Наклонная" }, + { key: "t_shape", label: "Т-образная" }, + { key: "dome", label: "Купольная" }, + { key: "built_in", label: "Встроенная" }, + { key: "telescopic", label: "Телескопическая" }, + { key: "island", label: "Островная" }, + ]}, + { key: "width", label: "Ширина, см", options: [ + { key: "50", label: "50" }, { key: "60", label: "60" }, + { key: "80", label: "80" }, { key: "90", label: "90" }, + ]}, + { key: "color", label: "Цвет", options: [ + { key: "inox", label: "Нерж. сталь" }, + { key: "black", label: "Чёрный" }, + { key: "white", label: "Белый" }, + { key: "black_glass", label: "Чёрное стекло" }, + ]}, + { key: "mode", label: "Режим работы", options: [ + { key: "exhaust", label: "Только отвод (вентиляция)" }, + { key: "recirc", label: "Только рециркуляция (фильтр)" }, + { key: "combi", label: "Оба режима" }, + ]}, + ], + features: [ + { key: "hi_perf", label: "Производительность 600+ м³/ч", hint: "сильно тянет — для большой кухни / wok" }, + { key: "perimeter", label: "Периметральная вытяжка", hint: "тянет с краёв — больше пара захватывает" }, + { key: "low_noise", label: "Тихая работа ≤50 дБ", hint: "не оглушает за столом" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "автоматическая работа в паре с варочной" }, + ], + }, + microwave: { + primary: [ + { key: "type", label: "Размещение", options: [ + { key: "builtin", label: "Встроенная" }, + { key: "freestanding", label: "Отдельная" }, + ]}, + { key: "volume", label: "Объём, л", options: [ + { key: "to20", label: "до 20" }, + { key: "20-25", label: "20–25" }, + { key: "25+", label: "25+" }, + ]}, + ], + features: [ + { key: "grill", label: "Гриль", hint: "запекает корочку сверху" }, + { key: "convection", label: "Конвекция", hint: "работает как маленькая духовка" }, + { key: "inverter", label: "Инвертор", hint: "плавная мощность — не пересушивает" }, + ], + }, + coffee: { + primary: [ + { key: "type", label: "Размещение", options: [ + { key: "builtin", label: "Встроенная" }, + { key: "freestanding", label: "Отдельная" }, + ]}, + { key: "tech", label: "Тип", options: [ + { key: "auto_grinder", label: "Автомат с кофемолкой" }, + { key: "capsule", label: "Капсульная" }, + { key: "manual", label: "Рожковая (бариста)" }, + ]}, + ], + features: [ + { key: "milk", label: "Капучинатор", hint: "автоматическое латте/капучино" }, + { key: "profiles", label: "Профили", hint: "у каждого свой размер/крепость" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "управление с телефона" }, + ], + }, + washer: { + primary: [ + { key: "type", label: "Размещение", options: [ + { key: "builtin", label: "Встроенная" }, + { key: "freestanding", label: "Отдельная" }, + ]}, + { key: "load", label: "Загрузка, кг", options: [ + { key: "to6", label: "до 6" }, + { key: "6-8", label: "6–8" }, + { key: "8-10", label: "8–10" }, + { key: "10+", label: "10+" }, + ]}, + { key: "depth", label: "Глубина", options: [ + { key: "slim", label: "Slim (до 45 см)" }, + { key: "standard", label: "Стандарт (60 см)" }, + ]}, + ], + features: [ + { key: "steam", label: "Пар", hint: "освежает без стирки, убивает аллергены" }, + { key: "dry", label: "Сушка", hint: "достал — и сразу в шкаф" }, + { key: "silent", label: "≤50 дБ", hint: "ночная стирка не разбудит" }, + { key: "smart", label: "Smart / Wi-Fi", hint: "запуск с телефона, уведомления" }, + ], + }, +}; + /* Бренды для каждой категории — для чипов с тирами. Сокращённый набор; полный список можно расширить из исходного HTML. */ const PODBOR_BRANDS = { diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 9449d84..fa39b8d 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -543,3 +543,250 @@ } .empty { text-align: center; color: var(--muted); padding: var(--s7) 0; } + +/* ============================================================ + Detail menu — list of categories with status + ============================================================ */ +.detail-list { + display: flex; + flex-direction: column; + gap: var(--s2); +} + +.detail-card { + display: flex; + align-items: center; + gap: var(--s3); + background: var(--card); + border: 1px solid var(--line-strong); + border-radius: var(--r-card); + padding: var(--s3) var(--s4); + cursor: pointer; + transition: background 0.12s; + text-align: left; +} + +.detail-card:active { background: var(--paper-2); } + +.detail-card.done { border-color: var(--accent-2); } + +.detail-icon { + width: 28px; + height: 28px; + display: grid; + place-items: center; + color: var(--ink); + flex-shrink: 0; +} + +.detail-icon svg { width: 22px; height: 22px; stroke-width: 1.4; } + +.detail-card.done .detail-icon { color: var(--accent-2); } + +.detail-text { flex: 1; min-width: 0; } + +.detail-name { + font-family: var(--font-ui); + font-size: 14.5px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); +} + +.detail-sum { + font-family: var(--font-mono); + font-size: 10.5px; + letter-spacing: 0.06em; + color: var(--muted); + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.detail-card.done .detail-sum { color: var(--ink-2); text-transform: none; letter-spacing: 0; font-family: var(--font-ui); font-size: 12.5px; } + +.detail-status { + flex-shrink: 0; + width: 22px; + height: 22px; + display: grid; + place-items: center; + color: var(--muted-2); +} + +.detail-status svg { width: 18px; height: 18px; } + +.detail-card.done .detail-status { + width: 22px; + height: 22px; + background: var(--accent-2); + color: var(--paper); + border-radius: var(--r-pill); +} + +.detail-card.done .detail-status svg { width: 14px; height: 14px; stroke-width: 2.5; } + +/* ============================================================ + Category detail screen + ============================================================ */ +.cat-detail-header { + display: flex; + align-items: center; + gap: var(--s3); + margin-bottom: var(--s4); +} + +.cat-detail-icon { + width: 30px; + height: 30px; + display: grid; + place-items: center; + color: var(--accent-2); +} + +.cat-detail-icon svg { width: 26px; height: 26px; stroke-width: 1.4; } + +.cat-detail-title { + font-family: var(--font-display); + font-style: italic; + font-size: 24px; + font-weight: 400; + margin: 0; + color: var(--ink); + flex: 1; +} + +.param-group { + display: flex; + flex-direction: column; + gap: 6px; + padding: var(--s3) 0; + border-top: 1px solid var(--line); +} + +.param-group:first-of-type { + border-top: none; + padding-top: 0; +} + +.param-label { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} + +/* ============================================================ + Accordion "Подробнее" + ============================================================ */ +.accordion-head { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--s3) var(--s4); + background: var(--card); + border: 1px solid var(--line-strong); + border-radius: var(--r-card); + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + cursor: pointer; +} + +.accordion-chev { + display: grid; + place-items: center; + color: var(--muted); + transition: transform 0.18s; +} + +.accordion-chev svg { width: 16px; height: 16px; transform: rotate(90deg); transition: transform 0.18s; } +.accordion-chev.open svg { transform: rotate(-90deg); } + +.accordion-body { + overflow: hidden; + max-height: 0; + transition: max-height 0.25s ease; + padding: 0; +} + +.accordion-body.open { + max-height: 2000px; + padding-top: var(--s3); +} + +.accordion-body > * + * { margin-top: var(--s3); } + +/* ============================================================ + Feature card (in accordion) + ============================================================ */ +.feature-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.feature { + display: grid; + grid-template-columns: 1fr 22px; + gap: var(--s2); + align-items: center; + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--r-tag); + padding: 10px 12px; + cursor: pointer; + text-align: left; + transition: all 0.12s; +} + +.feature:active { background: var(--paper-2); } +.feature.on { + border-color: var(--accent-2); + background: var(--paper-2); +} + +.feature-name { + grid-column: 1 / 2; + font-family: var(--font-ui); + font-size: 13.5px; + font-weight: 600; + color: var(--ink); + line-height: 1.2; +} + +.feature-hint { + grid-column: 1 / 2; + font-family: var(--font-ui); + font-size: 12px; + color: var(--muted); + line-height: 1.3; + margin-top: 2px; +} + +.feature-tick { + grid-column: 2 / 3; + grid-row: 1 / 3; + width: 22px; + height: 22px; + display: grid; + place-items: center; + border: 1px solid var(--line-strong); + border-radius: var(--r-pill); + align-self: center; +} + +.feature.on .feature-tick { + background: var(--accent-2); + color: var(--paper); + border-color: var(--accent-2); +} + +.feature.on .feature-tick svg { width: 12px; height: 12px; stroke-width: 2.5; } diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index b325cf4..42bb303 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -3,8 +3,11 @@ ============================================================ */ const Podbor = (function () { - const STORAGE_KEY = "zov-podbor-v2"; - const STEPS = ["intro", "categories", "pricing", "infra", "priorities", "brands", "summary"]; + const STORAGE_KEY = "zov-podbor-v3"; + const STEPS = ["intro", "categories", "detail", "pricing", "infra", "priorities", "brands", "summary"]; + + // Внутренний sub-state для шага «detail»: 'menu' | 'cat:' + let detailView = "menu"; let state = loadState(); let root = null; @@ -24,6 +27,7 @@ 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 }, ... } infra: { stove: "", vent: "" }, priorities: [], // ['balance','reviews',...] @@ -54,6 +58,7 @@ const Podbor = (function () { function go(step) { if (!STEPS.includes(step)) return; currentStep = step; + detailView = "menu"; // на любой переход detail возвращается в меню render(); window.scrollTo({ top: 0, behavior: "smooth" }); haptic && haptic("impact"); @@ -70,6 +75,7 @@ const Podbor = (function () { switch (currentStep) { 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 "infra": screen.appendChild(renderInfra()); break; case "priorities": screen.appendChild(renderPriorities()); break; @@ -105,7 +111,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(`
@@ -165,7 +171,7 @@ const Podbor = (function () {
${grid}
- +
`); @@ -183,6 +189,191 @@ const Podbor = (function () { return node; } + /* ===================== Step: detail — menu + per-category sub-screen ===================== */ + + function isCategoryFilled(catKey) { + const cat = state.per_cat[catKey]; + if (!cat || !cat.params) return false; + const params = PODBOR_PARAMS[catKey]?.primary || []; + return params.every(p => cat.params[p.key]); + } + + function renderDetail() { + if (!state.categories.length) { + return el(` +
+
Сначала выберите категории.
+
+ +
+
+ `); + } + if (detailView !== "menu" && detailView.startsWith("cat:")) { + const catKey = detailView.slice(4); + return renderCategoryDetail(catKey); + } + return renderDetailMenu(); + } + + function renderDetailMenu() { + const cards = state.categories.map(catKey => { + const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); + const filled = isCategoryFilled(catKey); + const summary = filled ? buildPerCatSummary(catKey) : "Заполнить параметры"; + return ` + + `; + }).join(""); + + const node = el(` +
+

Параметры
по категориям

+

Только главное: тип, размер, цвет. Технические фичи — в «Подробнее ↓», по желанию.

+
${cards}
+
+ + +
+
+ `); + node.querySelectorAll(".detail-card").forEach(c => { + c.addEventListener("click", () => { + detailView = "cat:" + c.dataset.cat; + render(); + }); + }); + bindNav(node); + return node; + } + + function buildPerCatSummary(catKey) { + const cat = state.per_cat[catKey]; + if (!cat || !cat.params) return "—"; + const params = PODBOR_PARAMS[catKey]?.primary || []; + const labels = params + .map(p => { + const opt = p.options.find(o => o.key === cat.params[p.key]); + return opt ? opt.label : null; + }) + .filter(Boolean); + return labels.join(" · ") || "—"; + } + + function renderCategoryDetail(catKey) { + const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); + const config = PODBOR_PARAMS[catKey]; + if (!config) { + return el(`
Параметры для «${cat?.label}» ещё не описаны.
`); + } + const catState = state.per_cat[catKey] || { params: {}, features: [], notes: "" }; + const isExpanded = catState._expanded || false; + + const primaryHtml = config.primary.map(p => { + const cur = catState.params?.[p.key] || ""; + return ` +
+
${p.label}
+
+ ${p.options.map(o => ` + + `).join("")} +
+
+ `; + }).join(""); + + const featuresHtml = config.features.map(f => { + const on = (catState.features || []).includes(f.key); + return ` + + `; + }).join(""); + + const node = el(` +
+
+ +
${ICONS[cat.icon] || ""}
+

${cat.label}

+
+ +
+
Главное
+ ${primaryHtml} +
+ + +
+
Технические фичи — необязательно. Если не отметите, AI выберет сам и пояснит в подборе.
+
${featuresHtml}
+ +
+ +
+ + +
+
+ `); + + // Главное — radio + node.querySelectorAll("[data-param]").forEach(b => { + b.addEventListener("click", () => { + const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" }; + cs.params = { ...(cs.params || {}), [b.dataset.param]: b.dataset.val }; + update({ per_cat: { ...state.per_cat, [catKey]: cs } }); + render(); + }); + }); + // Features — toggle + node.querySelectorAll("[data-feat]").forEach(b => { + b.addEventListener("click", () => { + const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" }; + const cur = cs.features || []; + cs.features = cur.includes(b.dataset.feat) ? cur.filter(x => x !== b.dataset.feat) : [...cur, b.dataset.feat]; + update({ per_cat: { ...state.per_cat, [catKey]: cs } }); + render(); + }); + }); + // Accordion + node.querySelector("[data-toggle='exp']").addEventListener("click", () => { + const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" }; + cs._expanded = !cs._expanded; + update({ per_cat: { ...state.per_cat, [catKey]: cs } }); + render(); + }); + // Notes + const ta = node.querySelector("textarea[data-bind='cat_notes']"); + if (ta) ta.addEventListener("input", e => { + const cs = state.per_cat[catKey] || { params: {}, features: [], notes: "" }; + cs.notes = e.target.value; + update({ per_cat: { ...state.per_cat, [catKey]: cs } }); + }); + // Back / save → menu + node.querySelector(".podbor-back").addEventListener("click", () => { detailView = "menu"; render(); }); + node.querySelector("#catBack").addEventListener("click", () => { detailView = "menu"; render(); }); + node.querySelector("#catSave").addEventListener("click", () => { detailView = "menu"; render(); haptic && haptic("success"); }); + return node; + } + /* ===================== Step: pricing (ценовой коридор по категориям) ===================== */ function renderPricing() { @@ -191,7 +382,7 @@ const Podbor = (function () {
Сначала выберите категории.
- +
`); @@ -234,7 +425,7 @@ const Podbor = (function () { ${totalLine}
- +
diff --git a/miniapp/index.html b/miniapp/index.html index 951c41f..5f3d3a8 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + +
@@ -21,9 +21,9 @@
- - - - + + + +