mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 17:44:48 +00:00
feat(miniapp): «Подбор техники» screen — 7-step picker (categories/niches/budget/infra/scenario/brands/summary) wired to /api/podbor
This commit is contained in:
parent
86cd4eb614
commit
d1165d5e4f
2314
design-drafts/02_Checklist_source.html
Normal file
2314
design-drafts/02_Checklist_source.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -175,10 +175,10 @@ function renderManagerHome(me) {
|
||||
|
||||
// Quick actions
|
||||
const quickActions = [
|
||||
{ icon: "camera", title: "Новый замер", subtitle: "С фото" },
|
||||
{ icon: "cube", title: "3D просмотр", subtitle: "Проекты" },
|
||||
{ icon: "bolt", title: "Коммуникации", subtitle: "Чек-лист" },
|
||||
{ icon: "package", title: "Каталог техники", subtitle: "Встройка" },
|
||||
{ icon: "camera", title: "Новый замер", subtitle: "С фото", href: null },
|
||||
{ icon: "cube", title: "3D просмотр", subtitle: "Проекты", href: null },
|
||||
{ icon: "bolt", title: "Коммуникации", subtitle: "Чек-лист", href: null },
|
||||
{ icon: "package", title: "Подбор техники", subtitle: "Встройка + AI", href: "#/podbor" },
|
||||
];
|
||||
app.appendChild(el(`<div class="section-head"><span class="label">Быстрые действия</span></div>`));
|
||||
const grid = el(`<div class="quick-grid"></div>`);
|
||||
@ -190,7 +190,11 @@ function renderManagerHome(me) {
|
||||
<div class="subtitle">${qa.subtitle}</div>
|
||||
</button>
|
||||
`);
|
||||
card.addEventListener("click", () => { haptic("impact"); tg?.showAlert?.(`«${qa.title}» — скоро`); });
|
||||
card.addEventListener("click", () => {
|
||||
haptic("impact");
|
||||
if (qa.href) location.hash = qa.href;
|
||||
else tg?.showAlert?.(`«${qa.title}» — скоро`);
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
app.appendChild(grid);
|
||||
@ -350,8 +354,16 @@ function renderError() {
|
||||
/* ----------------- Init ----------------- */
|
||||
async function init() {
|
||||
setupTelegram();
|
||||
// Hash-роутер: позволяет открывать подэкраны (например подбор) напрямую
|
||||
window.addEventListener("hashchange", routeByHash);
|
||||
|
||||
try {
|
||||
const me = await fetchMe();
|
||||
window.__zovMe = me; // кешируем профиль для подэкранов
|
||||
if (location.hash.startsWith("#/podbor")) {
|
||||
Podbor.mount(app);
|
||||
return;
|
||||
}
|
||||
if (me.role === "manager") renderManager(me);
|
||||
else renderClient(me);
|
||||
} catch (e) {
|
||||
@ -360,4 +372,16 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
function routeByHash() {
|
||||
if (location.hash.startsWith("#/podbor")) {
|
||||
Podbor.mount(app);
|
||||
} else {
|
||||
// Главный экран по роли
|
||||
const me = window.__zovMe;
|
||||
if (!me) { init(); return; }
|
||||
if (me.role === "manager") renderManager(me);
|
||||
else renderClient(me);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@ -45,4 +45,18 @@ const ICONS = {
|
||||
chat: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
|
||||
|
||||
home: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
|
||||
|
||||
arrow_left: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>`,
|
||||
|
||||
/* Категории техники — line, stroke 1.5 */
|
||||
cat_hob: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.6"/><circle cx="16" cy="8" r="2.5"/><circle cx="8" cy="16" r="2.5"/><circle cx="16" cy="16" r="1.6"/></svg>`,
|
||||
cat_fridge: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><path d="M5 10h14"/><path d="M9 5v3"/><path d="M9 14v3"/></svg>`,
|
||||
cat_dw: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M4 7h16"/><circle cx="8" cy="5" r=".6"/><circle cx="11" cy="5" r=".6"/><path d="M9 11c0 4 3 4 3 8"/><path d="M15 11c0 4-3 4-3 8"/></svg>`,
|
||||
cat_hood: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10 7 4h10l4 6"/><path d="M3 10v4h18v-4"/><path d="M9 18v3"/><path d="M15 18v3"/></svg>`,
|
||||
cat_oven: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><circle cx="7" cy="6" r=".6"/><circle cx="11" cy="6" r=".6"/><path d="M16 6h2"/><circle cx="12" cy="15" r="3"/></svg>`,
|
||||
cat_microwave: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M15 5v14"/><circle cx="18" cy="9" r=".6"/><path d="M18 13v3"/><path d="M5 9h7"/><path d="M5 13h5"/></svg>`,
|
||||
cat_coffee: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M17 8h1a4 4 0 0 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z"/><path d="M6 2v3"/><path d="M10 2v3"/><path d="M14 2v3"/></svg>`,
|
||||
cat_washer: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><circle cx="12" cy="14" r="5"/><circle cx="8" cy="6" r=".6"/><circle cx="11" cy="6" r=".6"/><path d="M14 6h3"/><path d="M9 14a3 3 0 0 1 6 0"/></svg>`,
|
||||
|
||||
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2"><path d="M20 6 9 17l-5-5"/></svg>`,
|
||||
};
|
||||
|
||||
101
miniapp/assets/podbor.config.js
Normal file
101
miniapp/assets/podbor.config.js
Normal file
@ -0,0 +1,101 @@
|
||||
/* ============================================================
|
||||
Подбор техники — статические данные (адаптация 02_Чек-лист_клиенту.html)
|
||||
============================================================ */
|
||||
|
||||
const PODBOR_CATEGORIES = [
|
||||
{ key: "fridge", icon: "cat_fridge", label: "Холодильник" },
|
||||
{ key: "hob", icon: "cat_hob", label: "Варочная панель" },
|
||||
{ key: "oven", icon: "cat_oven", label: "Духовой шкаф" },
|
||||
{ key: "dw", icon: "cat_dw", label: "Посудомоечная" },
|
||||
{ key: "hood", icon: "cat_hood", label: "Вытяжка" },
|
||||
{ key: "microwave", icon: "cat_microwave", label: "Микроволновка" },
|
||||
{ key: "coffee", icon: "cat_coffee", label: "Кофемашина" },
|
||||
{ key: "washer", icon: "cat_washer", label: "Стиральная машина" },
|
||||
];
|
||||
|
||||
const PODBOR_BUDGET_TIERS = [
|
||||
{ key: "premium", label: "Премиум", hint: "лучшее без оглядки на цену" },
|
||||
{ key: "middle", 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 = {
|
||||
stove: [
|
||||
{ key: "induction", label: "Индукция / 380 В" },
|
||||
{ key: "el_220", label: "Электрика 220 В" },
|
||||
{ key: "gas", label: "Газ" },
|
||||
{ key: "any", label: "Не знаю / любой" },
|
||||
],
|
||||
vent: [
|
||||
{ key: "shaft", label: "Шахта вентиляции есть" },
|
||||
{ key: "no_shaft", 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" },
|
||||
];
|
||||
|
||||
/* Бренды для каждой категории — для чипов с тирами.
|
||||
Сокращённый набор; полный список можно расширить из исходного HTML. */
|
||||
const PODBOR_BRANDS = {
|
||||
fridge: {
|
||||
premium: ["Liebherr", "Miele", "Sub-Zero", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Indesit", "Beko", "Hotpoint"],
|
||||
},
|
||||
hob: {
|
||||
premium: ["Miele", "Gaggenau", "AEG"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux", "Hansa"],
|
||||
budget: ["Hotpoint", "Beko", "Indesit"],
|
||||
},
|
||||
oven: {
|
||||
premium: ["Miele", "Gaggenau", "Neff"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux", "AEG"],
|
||||
budget: ["Hansa", "Beko", "Hotpoint"],
|
||||
},
|
||||
dw: {
|
||||
premium: ["Miele", "Asko", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux"],
|
||||
budget: ["Hansa", "Beko", "Indesit"],
|
||||
},
|
||||
hood: {
|
||||
premium: ["Miele", "Falmec", "Faber"],
|
||||
middle: ["Bosch", "Siemens", "Elica"],
|
||||
budget: ["Hansa", "Hotpoint", "Maunfeld"],
|
||||
},
|
||||
microwave: {
|
||||
premium: ["Miele", "Neff"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Whirlpool", "Hansa", "Beko"],
|
||||
},
|
||||
coffee: {
|
||||
premium: ["Miele", "Jura", "De'Longhi PrimaDonna"],
|
||||
middle: ["De'Longhi", "Saeco", "Bosch"],
|
||||
budget: ["Krups", "Philips"],
|
||||
},
|
||||
washer: {
|
||||
premium: ["Miele", "Asko", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Indesit", "Hotpoint", "Beko"],
|
||||
},
|
||||
};
|
||||
434
miniapp/assets/podbor.css
Normal file
434
miniapp/assets/podbor.css
Normal file
@ -0,0 +1,434 @@
|
||||
/* ============================================================
|
||||
Подбор техники — стили (Editorial Calm)
|
||||
============================================================ */
|
||||
|
||||
/* ----- Header ----- */
|
||||
.podbor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--s4);
|
||||
}
|
||||
|
||||
.podbor-back {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.podbor-back svg { width: 20px; height: 20px; }
|
||||
|
||||
.podbor-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ----- Progress ----- */
|
||||
.podbor-progress {
|
||||
margin-bottom: var(--s5);
|
||||
}
|
||||
|
||||
.podbor-progress-bar {
|
||||
height: 2px;
|
||||
background: var(--line);
|
||||
border-radius: var(--r-pill);
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.podbor-progress-bar .bar {
|
||||
height: 100%;
|
||||
background: var(--accent-2);
|
||||
border-radius: inherit;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.podbor-progress-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.podbor-progress-meta .num { color: var(--ink); }
|
||||
|
||||
/* ----- Step container ----- */
|
||||
.podbor-screen { animation: fadeIn 0.2s ease; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
.podbor-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s4);
|
||||
}
|
||||
|
||||
.display-title {
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.display-title .accent { color: var(--accent-2); }
|
||||
|
||||
.lede {
|
||||
font-size: 14.5px;
|
||||
line-height: 1.45;
|
||||
color: var(--ink-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ----- Form basics ----- */
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea,
|
||||
.field-inline input {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 15px;
|
||||
color: var(--ink);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-btn);
|
||||
padding: 11px 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.12s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field textarea:focus,
|
||||
.field-inline input:focus { border-color: var(--accent-2); }
|
||||
|
||||
.field textarea { resize: vertical; min-height: 64px; font-family: var(--font-ui); }
|
||||
|
||||
.form-row { display: flex; flex-direction: column; gap: var(--s2); }
|
||||
.form-row.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
|
||||
|
||||
/* ----- Block grouping ----- */
|
||||
.block {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-card);
|
||||
padding: var(--s4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s3);
|
||||
}
|
||||
|
||||
.block-head {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ----- Categories grid ----- */
|
||||
.cat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--s2);
|
||||
}
|
||||
|
||||
.cat-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-card);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
min-height: 84px;
|
||||
}
|
||||
|
||||
.cat-card:active { background: var(--paper-2); }
|
||||
|
||||
.cat-card.active {
|
||||
border-color: var(--ink);
|
||||
background: var(--paper-2);
|
||||
}
|
||||
|
||||
.cat-card .cat-icon {
|
||||
width: 26px; height: 26px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.cat-card .cat-icon svg { width: 22px; height: 22px; stroke-width: 1.4; }
|
||||
|
||||
.cat-card .cat-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.cat-card .cat-check {
|
||||
position: absolute;
|
||||
top: 10px; right: 10px;
|
||||
width: 18px; height: 18px;
|
||||
background: var(--accent-2);
|
||||
color: var(--paper);
|
||||
border-radius: var(--r-pill);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
|
||||
.cat-card .cat-check svg { width: 12px; height: 12px; stroke-width: 2.5; }
|
||||
|
||||
/* ----- Niches ----- */
|
||||
.niche-list { display: flex; flex-direction: column; gap: var(--s2); }
|
||||
.niche-row { display: flex; flex-direction: column; gap: 4px; }
|
||||
.niche-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.niche-inputs { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
|
||||
.niche-inputs input {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
padding: 9px 10px;
|
||||
text-align: center;
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r-tag);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.budget-list { display: flex; flex-direction: column; gap: var(--s2); }
|
||||
.field-inline {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
|
||||
font-family: var(--font-ui); font-size: 14px; color: var(--ink-2);
|
||||
}
|
||||
.field-inline input {
|
||||
width: 130px; padding: 8px 10px; font-family: var(--font-mono); font-size: 13px;
|
||||
background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-tag);
|
||||
text-align: right; color: var(--ink);
|
||||
}
|
||||
|
||||
/* ----- Option chips ----- */
|
||||
.opt-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
.opt {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--r-pill);
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--paper);
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.opt:active { transform: scale(0.97); }
|
||||
|
||||
.opt.on {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
|
||||
/* ----- 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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.chip {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--r-tag);
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--paper);
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chip.status-preferred {
|
||||
background: var(--accent-2);
|
||||
color: var(--paper);
|
||||
border-color: var(--accent-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chip.status-preferred::before { content: "★ "; }
|
||||
|
||||
.chip.status-acceptable {
|
||||
background: var(--paper-2);
|
||||
color: var(--ink);
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
|
||||
.chip.status-acceptable::before { content: "✓ "; }
|
||||
|
||||
/* ----- Summary ----- */
|
||||
.summary-block { gap: 8px; }
|
||||
.summary-block .kv {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
}
|
||||
.summary-block .kv:last-child { border-bottom: none; }
|
||||
.summary-block .kv span {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.summary-block .kv strong { font-weight: 600; color: var(--ink); }
|
||||
|
||||
/* ----- CTA / buttons ----- */
|
||||
.podbor-cta-row {
|
||||
display: flex; gap: var(--s2);
|
||||
margin-top: var(--s3);
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
flex: 1;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
padding: 12px var(--s4);
|
||||
border-radius: var(--r-btn);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.12s, background 0.12s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.btn-primary:active { opacity: 0.85; }
|
||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line-strong);
|
||||
}
|
||||
|
||||
.btn-secondary:active { background: var(--paper-2); }
|
||||
|
||||
/* ----- Submit result ----- */
|
||||
.submit-result { margin-top: var(--s3); }
|
||||
|
||||
.submit-result .success {
|
||||
display: flex; align-items: center; gap: var(--s3);
|
||||
background: var(--paper-2);
|
||||
border: 1px solid var(--accent-2);
|
||||
border-radius: var(--r-card);
|
||||
padding: var(--s4);
|
||||
}
|
||||
|
||||
.submit-result .success-icon {
|
||||
width: 36px; height: 36px;
|
||||
background: var(--accent-2);
|
||||
color: var(--paper);
|
||||
border-radius: var(--r-pill);
|
||||
display: grid; place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.submit-result .success-icon svg { width: 20px; height: 20px; stroke-width: 2.5; }
|
||||
|
||||
.submit-result .success-title {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.submit-result .success-sub {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.submit-result .error {
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
border: 1px solid rgba(192, 57, 43, 0.30);
|
||||
color: #8C3F1E;
|
||||
border-radius: var(--r-card);
|
||||
padding: var(--s3) var(--s4);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.spinner-inline {
|
||||
display: inline-block;
|
||||
width: 14px; height: 14px;
|
||||
border: 1.5px solid rgba(251,247,240,0.3);
|
||||
border-top-color: var(--paper);
|
||||
border-radius: var(--r-pill);
|
||||
animation: spin 0.7s linear infinite;
|
||||
margin-right: 6px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.empty { text-align: center; color: var(--muted); padding: var(--s7) 0; }
|
||||
519
miniapp/assets/podbor.js
Normal file
519
miniapp/assets/podbor.js
Normal file
@ -0,0 +1,519 @@
|
||||
/* ============================================================
|
||||
Подбор техники — render, state, navigation, submit
|
||||
============================================================ */
|
||||
|
||||
const Podbor = (function () {
|
||||
const STORAGE_KEY = "zov-podbor-v1";
|
||||
const STEPS = ["intro", "categories", "context", "infra", "scenario", "brands", "summary"];
|
||||
|
||||
let state = loadState();
|
||||
let root = null;
|
||||
let currentStep = "intro";
|
||||
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch (e) {}
|
||||
return defaultState();
|
||||
}
|
||||
|
||||
function defaultState() {
|
||||
return {
|
||||
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, ... }
|
||||
infra: { stove: "", vent: "" },
|
||||
scenario: { family: "", cooking: "", techniques: [], guests: "" },
|
||||
brands: {}, // { fridge: {Bosch:'preferred', Liebherr:'preferred'}, ... }
|
||||
notes: "",
|
||||
};
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
|
||||
}
|
||||
|
||||
function update(patch) {
|
||||
state = { ...state, ...patch };
|
||||
saveState();
|
||||
}
|
||||
|
||||
/* ===================== Render entry ===================== */
|
||||
|
||||
function mount(container) {
|
||||
root = container;
|
||||
document.body.classList.remove("has-bottom-nav");
|
||||
const oldNav = document.getElementById("bottom-nav");
|
||||
if (oldNav) oldNav.remove();
|
||||
render();
|
||||
}
|
||||
|
||||
function go(step) {
|
||||
if (!STEPS.includes(step)) return;
|
||||
currentStep = step;
|
||||
render();
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
haptic && haptic("impact");
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!root) return;
|
||||
root.innerHTML = "";
|
||||
root.appendChild(renderHeader());
|
||||
root.appendChild(renderProgress());
|
||||
const screen = el(`<div class="podbor-screen"></div>`);
|
||||
root.appendChild(screen);
|
||||
|
||||
switch (currentStep) {
|
||||
case "intro": screen.appendChild(renderIntro()); break;
|
||||
case "categories": screen.appendChild(renderCategories()); break;
|
||||
case "context": screen.appendChild(renderContext()); break;
|
||||
case "infra": screen.appendChild(renderInfra()); break;
|
||||
case "scenario": screen.appendChild(renderScenario()); break;
|
||||
case "brands": screen.appendChild(renderBrands()); break;
|
||||
case "summary": screen.appendChild(renderSummary()); break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Header & progress ===================== */
|
||||
|
||||
function renderHeader() {
|
||||
const h = el(`
|
||||
<header class="podbor-header">
|
||||
<button class="podbor-back" aria-label="Назад">${ICONS.arrow_left}</button>
|
||||
<div class="podbor-title">Подбор техники</div>
|
||||
<div style="width:28px"></div>
|
||||
</header>
|
||||
`);
|
||||
h.querySelector(".podbor-back").addEventListener("click", () => {
|
||||
const idx = STEPS.indexOf(currentStep);
|
||||
if (idx <= 0) {
|
||||
// Выход из подбора в главный экран кабинета
|
||||
location.hash = "";
|
||||
location.reload();
|
||||
} else {
|
||||
go(STEPS[idx - 1]);
|
||||
}
|
||||
});
|
||||
return h;
|
||||
}
|
||||
|
||||
function renderProgress() {
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
/* ===================== Step: intro ===================== */
|
||||
|
||||
function renderIntro() {
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Подбор техники<br><span class="accent">для клиента</span></h2>
|
||||
<p class="lede">7 коротких шагов. Указываем категории, бюджет, инфраструктуру и предпочтения. Дальше AI сам соберёт предложение.</p>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="field">
|
||||
<span class="field-label">Клиент</span>
|
||||
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row two-col">
|
||||
<label class="field">
|
||||
<span class="field-label">Телефон</span>
|
||||
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ...">
|
||||
</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 class="podbor-cta-row">
|
||||
<button class="btn-primary" data-go="categories">Начать</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
bindInputs(node);
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: categories ===================== */
|
||||
|
||||
function renderCategories() {
|
||||
const grid = PODBOR_CATEGORIES.map(c => `
|
||||
<button class="cat-card${state.categories.includes(c.key) ? " active" : ""}" data-cat="${c.key}">
|
||||
<div class="cat-icon">${ICONS[c.icon] || ""}</div>
|
||||
<div class="cat-label">${c.label}</div>
|
||||
${state.categories.includes(c.key) ? `<div class="cat-check">${ICONS.check}</div>` : ""}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Какую технику<br><span class="accent">подбираем?</span></h2>
|
||||
<p class="lede">Выберите все категории, что нужно подобрать клиенту.</p>
|
||||
<div class="cat-grid">${grid}</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="intro">Назад</button>
|
||||
<button class="btn-primary" data-go="context">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll(".cat-card").forEach(card => {
|
||||
card.addEventListener("click", () => {
|
||||
const cat = card.dataset.cat;
|
||||
const next = state.categories.includes(cat)
|
||||
? state.categories.filter(x => x !== cat)
|
||||
: [...state.categories, cat];
|
||||
update({ categories: next });
|
||||
render();
|
||||
});
|
||||
});
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: context (ниши + бюджет по категориям) ===================== */
|
||||
|
||||
function renderContext() {
|
||||
const builtinCats = ["fridge", "hob", "oven", "dw"]; // встройка
|
||||
const niches = builtinCats.filter(c => state.categories.includes(c)).map(c => {
|
||||
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
|
||||
const n = state.niches[c] || {};
|
||||
return `
|
||||
<div class="niche-row">
|
||||
<div class="niche-label">${cat.label}</div>
|
||||
<div class="niche-inputs">
|
||||
<input type="number" data-niche="${c}.w" value="${n.w || ""}" placeholder="Ш, мм">
|
||||
<input type="number" data-niche="${c}.h" value="${n.h || ""}" placeholder="В, мм">
|
||||
<input type="number" data-niche="${c}.d" value="${n.d || ""}" placeholder="Г, мм">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const budgets = state.categories.map(c => {
|
||||
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
|
||||
const v = state.budget_by_cat[c] || "";
|
||||
return `
|
||||
<label class="field-inline">
|
||||
<span>${cat.label}</span>
|
||||
<input type="number" data-budget="${c}" value="${v}" placeholder="₽">
|
||||
</label>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Размеры<br><span class="accent">и бюджет</span></h2>
|
||||
<p class="lede">Если планируется встройка — укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.</p>
|
||||
|
||||
${niches ? `
|
||||
<div class="block">
|
||||
<div class="block-head">Ниши под встройку, мм</div>
|
||||
<div class="niche-list">${niches}</div>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
${budgets ? `
|
||||
<div class="block">
|
||||
<div class="block-head">Бюджет по категориям, ₽</div>
|
||||
<div class="budget-list">${budgets}</div>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="categories">Назад</button>
|
||||
<button class="btn-primary" data-go="infra">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll("[data-niche]").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 });
|
||||
});
|
||||
});
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: infra ===================== */
|
||||
|
||||
function renderInfra() {
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
||||
<p class="lede">Газ или электрика — главный вопрос для варочной. Шахта вентиляции — для вытяжки.</p>
|
||||
<div class="block">
|
||||
<div class="block-head">Подключение варочной</div>
|
||||
<div class="opt-list">
|
||||
${PODBOR_INFRA.stove.map(o => `
|
||||
<button class="opt${state.infra.stove === o.key ? " on" : ""}" data-infra="stove" data-val="${o.key}">${o.label}</button>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="block-head">Вентиляция для вытяжки</div>
|
||||
<div class="opt-list">
|
||||
${PODBOR_INFRA.vent.map(o => `
|
||||
<button class="opt${state.infra.vent === o.key ? " on" : ""}" data-infra="vent" data-val="${o.key}">${o.label}</button>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="context">Назад</button>
|
||||
<button class="btn-primary" data-go="scenario">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll("[data-infra]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
update({ infra: { ...state.infra, [b.dataset.infra]: b.dataset.val } });
|
||||
render();
|
||||
});
|
||||
});
|
||||
bindNav(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Step: scenario ===================== */
|
||||
|
||||
function renderScenario() {
|
||||
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_FAMILY.map(o => `
|
||||
<button class="opt${state.scenario.family === o.key ? " on" : ""}" data-scenario="family" data-val="${o.key}">${o.label}</button>
|
||||
`).join("")}
|
||||
</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">
|
||||
<button class="btn-secondary" data-go="infra">Назад</button>
|
||||
<button class="btn-primary" data-go="brands">Дальше</button>
|
||||
</div>
|
||||
</section>
|
||||
`);
|
||||
node.querySelectorAll("[data-scenario]").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 next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
|
||||
update({ scenario: { ...state.scenario, techniques: 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 tierBlock = (tier) => `
|
||||
<div class="tier-row">
|
||||
<div class="tier-label">${PODBOR_BUDGET_TIERS.find(t => t.key === tier).label}</div>
|
||||
<div class="brand-chips">
|
||||
${(brands[tier] || []).map(b => {
|
||||
const status = catState[b] || "none";
|
||||
return `<button class="chip status-${status}" data-cat="${catKey}" data-brand="${b}">${b}</button>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return `
|
||||
<div class="block">
|
||||
<div class="block-head">${cat.label}</div>
|
||||
<div class="hint">★ Тап — предпочтительно · ✓ Двойной — допустимо · — Третий — снять</div>
|
||||
${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")}
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Бренды<br><span class="accent">по категориям</span></h2>
|
||||
<p class="lede">Какие марки уважаете, какие — допустимы. AI сначала пробует preferred.</p>
|
||||
${blocks}
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="scenario">Назад</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 node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Готово<br><span class="accent">к подбору</span></h2>
|
||||
<p class="lede">Проверьте и отправьте — AI вернёт предложение в чат с ботом.</p>
|
||||
<div class="block summary-block">
|
||||
<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>${PODBOR_FAMILY.find(f => f.key === state.scenario.family)?.label || "—"}</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>
|
||||
|
||||
<label class="field">
|
||||
<span class="field-label">Дополнительные пожелания</span>
|
||||
<textarea data-bind="notes" rows="3" placeholder="Что-то особенное от клиента?">${state.notes || ""}</textarea>
|
||||
</label>
|
||||
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="brands">Назад</button>
|
||||
<button class="btn-primary" id="submitBtn">Отправить · AI подберёт</button>
|
||||
</div>
|
||||
|
||||
<div id="submitResult" class="submit-result"></div>
|
||||
</section>
|
||||
`);
|
||||
bindInputs(node);
|
||||
bindNav(node);
|
||||
node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node));
|
||||
return node;
|
||||
}
|
||||
|
||||
/* ===================== Submit ===================== */
|
||||
|
||||
async function onSubmit(node) {
|
||||
const btn = node.querySelector("#submitBtn");
|
||||
const result = node.querySelector("#submitResult");
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-inline"></span> AI думает...';
|
||||
result.innerHTML = "";
|
||||
|
||||
if (!BACKEND_URL) {
|
||||
result.innerHTML = `<div class="error">BACKEND_URL не настроен (dev-режим).</div>`;
|
||||
btn.disabled = false; btn.textContent = "Отправить · AI подберёт"; return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}?path=podbor`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
initData: tg?.initData || "",
|
||||
checklist: state,
|
||||
client_name: state.client_name,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
|
||||
} else {
|
||||
result.innerHTML = `
|
||||
<div class="success">
|
||||
<div class="success-icon">${ICONS.check}</div>
|
||||
<div>
|
||||
<div class="success-title">Подбор отправлен в чат бота</div>
|
||||
<div class="success-sub">Лид #${(data.id || "").slice(0, 6)} · откройте Telegram</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
haptic && haptic("success");
|
||||
}
|
||||
} catch (e) {
|
||||
result.innerHTML = `<div class="error">Сеть: ${e.message}</div>`;
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Отправить ещё раз";
|
||||
}
|
||||
|
||||
/* ===================== Helpers ===================== */
|
||||
|
||||
function bindInputs(node) {
|
||||
node.querySelectorAll("[data-bind]").forEach(inp => {
|
||||
inp.addEventListener("input", e => {
|
||||
update({ [e.target.dataset.bind]: e.target.value });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindNav(node) {
|
||||
node.querySelectorAll("[data-go]").forEach(b => {
|
||||
b.addEventListener("click", () => go(b.dataset.go));
|
||||
});
|
||||
}
|
||||
|
||||
return { mount, go, getState: () => state, reset: () => { state = defaultState(); saveState(); render(); } };
|
||||
})();
|
||||
@ -12,7 +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=20260509j">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260509k">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260509k">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
@ -20,7 +21,9 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js?v=20260509j"></script>
|
||||
<script src="assets/app.js?v=20260509j"></script>
|
||||
<script src="assets/icons.js?v=20260509k"></script>
|
||||
<script src="assets/podbor.config.js?v=20260509k"></script>
|
||||
<script src="assets/podbor.js?v=20260509k"></script>
|
||||
<script src="assets/app.js?v=20260509k"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user