mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 16:24:50 +00:00
1. MODEL COUNT SELECTOR (strategy step): - new PODBOR_MODEL_COUNTS [3/5/7] - state.model_count default '5' - UI on strategy page with description (быстро/оптимально/максимум) 2. AI PROMPT EXPANDED: - new field: manual_search_query — for Google search instruction PDF - new specs object per model: dimensions_mm/volume_l/weight_kg/noise_db/energy_class/color - 'specs ОБЯЗАТЕЛЬНЫ для проектирования кухни' explicit rule - reads checklist.model_count to determine how many models per category - max_tokens 4000 → 8000 (room for richer responses) 3. MODEL CARD RICHER: - _renderSpecsBlock — characteristics in 2-col grid, dimensions highlighted - _renderUtilityLinks — Google search buttons for инструкция (PDF) + Схема установки - Specs critical for ZOV kitchen design (manager needs to verify niche fits) 4. EXPORT BUTTONS: - 'Скачать HTML' — generates standalone HTML with inline styles, downloads as file - 'Печать → PDF' — opens new window with cleaned layout + auto-prints - User can save as PDF via system print dialog 5. PREVIEW updated with realistic specs/manual_query for all 3 fridges
483 lines
25 KiB
HTML
483 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<meta name="theme-color" content="#FBF7F0">
|
||
<title>Превью отчёта · ЗОВ</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||
<link rel="stylesheet" href="assets/styles.css?v=20260511e">
|
||
<link rel="stylesheet" href="assets/podbor.css?v=20260511e">
|
||
<style>
|
||
body { padding: 24px 16px; background: var(--paper); }
|
||
.wrap { max-width: 720px; margin: 0 auto; }
|
||
.preview-kicker {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-bottom: 6px;
|
||
}
|
||
.preview-title {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-size: 32px;
|
||
line-height: 1.05;
|
||
letter-spacing: -0.02em;
|
||
margin: 0 0 8px;
|
||
color: var(--ink);
|
||
}
|
||
.preview-lede {
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
margin: 0 0 24px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="preview-kicker">Preview · Inline Report v1</div>
|
||
<h1 class="preview-title">Отчёт<br><em style="color: var(--accent-2)">после подбора</em></h1>
|
||
<p class="preview-lede">Как будет выглядеть страница для менеджера и клиента после нажатия «Отправить». Mock-данные.</p>
|
||
|
||
<div id="reportRoot"></div>
|
||
</div>
|
||
|
||
<script src="assets/icons.js?v=20260511e"></script>
|
||
<script src="assets/podbor.config.js?v=20260511e"></script>
|
||
<script>
|
||
/* Mock-данные — имитируем AI-ответ с DNS+WB+Я.Маркет обогащением */
|
||
const MOCK_AI = {
|
||
summary: "Подобран комплект Haier для семьи из 3 человек, бюджет средний. Холодильник с NoFrost + индукционная варочная Haier и компактная духовка с конвекцией. Все три прибора одной марки для дизайн-единства и тише в эксплуатации.",
|
||
by_category: {
|
||
fridge: {
|
||
analysis: "В этом бюджете 2026 года Haier и Korting вытеснили Bosch по соотношению цена/качество. Haier C4F744CMG — оптимальный двухкамерный по тишине и объёму, Liebherr — премиум-вариант с зоной свежести, Бирюса — бюджетная страховка.",
|
||
models: [
|
||
{
|
||
brand: "Haier", model: "C4F744CMG",
|
||
price_min_rub: 89990, price_max_rub: 105000,
|
||
highlights: ["Total NoFrost (не нужно размораживать)", "Инвертор (тише и -30% энергии)", "Класс A++ (~30% экономии)"],
|
||
pros: ["тихий 36 дБ — на 4 дБ тише среднего по сегменту", "большой объём 463 л против 380 л у Bosch в этой цене", "инвертор + класс A++ — экономия ~30% против A+ моделей", "10 лет гарантии на компрессор"],
|
||
cons: ["глубина 660 мм — на 60 мм больше стандартной ниши, проверить на замере", "нет зоны свежести BioFresh — в этом плане Liebherr заметно лучше"],
|
||
reasoning: "Лучший по цена/качество в среднем сегменте. Тише и больше Bosch в той же цене, но без премиум-зоны свежести.",
|
||
specs: { dimensions_mm: "595×660×2000", volume_l: 463, weight_kg: 75, noise_db: 36, energy_class: "A++", color: "Нержавеющая сталь" },
|
||
manual_search_query: "Haier C4F744CMG manual инструкция pdf",
|
||
tier: "middle",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier",
|
||
price_min_rub: 89990, price_max_rub: 105000,
|
||
rating_max: 4.7, reviews_total: 1242, stores_count: 12,
|
||
citilink: { url: "https://www.citilink.ru/product/haier-123/", price_min_rub: 92000 },
|
||
ozon: { url: "https://www.ozon.ru/product/haier-c4f744cmg/", price_min_rub: 89990 },
|
||
wb: { url: "https://www.wildberries.ru/catalog/123/detail.aspx", price_min_rub: 94500 },
|
||
yamarket: { url: "https://market.yandex.ru/product--haier-c4f744cmg/123", price_min_rub: 105000 }
|
||
}
|
||
},
|
||
{
|
||
brand: "Liebherr", model: "CNd 5223",
|
||
price_min_rub: 134900, price_max_rub: 154000,
|
||
highlights: ["BioFresh (зона свежести)", "NoFrost", "SoftClose"],
|
||
pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"],
|
||
cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"],
|
||
reasoning: "Премиум-выбор для тех, кому важна зона свежести и тишина. Переплата ~50% за бренд и BioFresh.",
|
||
specs: { dimensions_mm: "597×675×2010", volume_l: 372, weight_kg: 89, noise_db: 34, energy_class: "A+++", color: "Нержавеющая сталь SmartSteel" },
|
||
manual_search_query: "Liebherr CNd 5223 manual инструкция pdf",
|
||
tier: "premium",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr",
|
||
rating_max: 4.9, reviews_total: 873, stores_count: 4,
|
||
ozon: { url: "https://www.ozon.ru/product/liebherr-cnd5223/", price_min_rub: 134900 },
|
||
citilink: { url: "https://www.citilink.ru/product/liebherr-cnd5223/", price_min_rub: 142000 },
|
||
yamarket: { url: "https://market.yandex.ru/product--liebherr-cnd5223/", price_min_rub: 154000 }
|
||
}
|
||
},
|
||
{
|
||
brand: "Бирюса", model: "M124",
|
||
price_min_rub: 39990,
|
||
highlights: ["LowFrost (размораживать только мороз)", "класс A+"],
|
||
pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"],
|
||
cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"],
|
||
reasoning: "Страховочный бюджет-вариант если клиент не хочет переплачивать. Российский, доступный, надёжный.",
|
||
specs: { dimensions_mm: "580×600×1450", volume_l: 240, weight_kg: 52, noise_db: 42, energy_class: "A+", color: "Белый" },
|
||
manual_search_query: "Бирюса M124 инструкция pdf",
|
||
tier: "budget",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa",
|
||
rating_max: 4.3, reviews_total: 2100, stores_count: 5,
|
||
ozon: { url: "https://www.ozon.ru/product/biryusa-m124/", price_min_rub: 39990 },
|
||
citilink: { url: "https://www.citilink.ru/product/biryusa-m124/", price_min_rub: 41500 },
|
||
wb: { url: "https://www.wildberries.ru/catalog/biryusa-m124/", price_min_rub: 42990 },
|
||
dns: { url: "https://www.dns-shop.ru/product/biryusa-m124/", price_min_rub: 44900 }
|
||
}
|
||
}
|
||
]
|
||
},
|
||
hob: {
|
||
analysis: "В 2026 году индукционные варочные среднего сегмента — это Haier и Korting. Bosch ещё доступен через ПИ, но цена выше на 25%. Все три модели — 60 см, 4 зоны, индукция.",
|
||
models: [
|
||
{
|
||
brand: "Haier", label: "Haier",
|
||
model: "HHX-Y64NFB",
|
||
price_min_rub: 39990, price_max_rub: 48000,
|
||
highlights: ["Индукция (магнитный нагрев посуды)", "Booster (кипятит за минуту)", "Сенсорное управление"],
|
||
pros: ["4 индукционные зоны 60 см — стандарт", "Booster на 2 зонах — кипятит литр за 60 сек", "защита от детей блокировкой", "класс A — экономия энергии"],
|
||
cons: ["требует индукционной посуды (магнитное дно)", "нет FlexZone — нельзя объединить две зоны под крупную сковороду"],
|
||
reasoning: "Базовая индукция от лидера рынка 2026 РФ. Идеальна когда не нужны премиум-фичи.",
|
||
tier: "middle",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier+hob",
|
||
rating_max: 4.6, reviews_total: 845, stores_count: 4,
|
||
ozon: { url: "https://www.ozon.ru/product/haier-hhx-y64nfb/", price_min_rub: 39990 },
|
||
citilink: { url: "https://www.citilink.ru/product/haier-hhx-y64nfb/", price_min_rub: 42500 },
|
||
wb: { url: "#", price_min_rub: 44990 },
|
||
yamarket: { url: "#", price_min_rub: 48000 }
|
||
}
|
||
},
|
||
{
|
||
brand: "Korting", model: "HI 64560 BB",
|
||
price_min_rub: 54990, price_max_rub: 62000,
|
||
highlights: ["Индукция", "FlexZone (объединение зон)", "9 уровней мощности"],
|
||
pros: ["FlexZone — объединяет 2 зоны для wok или большой кастрюли", "более чувствительный сенсор чем у Haier (9 vs 4 деления)", "5 лет официальной гарантии в РФ"],
|
||
cons: ["цена выше базового Haier на 38%", "управление сенсорное — иногда срабатывает по случайному касанию"],
|
||
reasoning: "Премиум-функционал FlexZone за разумные деньги. Для тех, кто часто готовит в wok.",
|
||
tier: "middle",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Korting",
|
||
rating_max: 4.8, reviews_total: 432, stores_count: 3,
|
||
ozon: { url: "https://www.ozon.ru/product/korting-hi64560bb/", price_min_rub: 54990 },
|
||
citilink: { url: "https://www.citilink.ru/product/korting-hi64560bb/", price_min_rub: 58000 },
|
||
yamarket: { url: "https://market.yandex.ru/product--korting-hi64560bb/", price_min_rub: 62000 }
|
||
}
|
||
},
|
||
{
|
||
brand: "Bosch", model: "PUE611BB5E ⚠ПИ",
|
||
price_min_rub: 64990, price_max_rub: 78000,
|
||
highlights: ["Индукция", "PowerBoost", "PerfectCook (сенсор посуды)"],
|
||
pros: ["PerfectCook — сенсор определяет посуду и держит температуру", "лучший дизайн без рамки", "немецкая надёжность"],
|
||
cons: ["параллельный импорт — гарантия только продавца, не Bosch", "цена выше Haier на 65% за похожий функционал", "ожидание поставки 3-5 недель"],
|
||
reasoning: "Премиум-выбор для эстетов. Доступен только через ПИ, переплата за бренд значительная.",
|
||
tier: "middle",
|
||
enriched: {
|
||
image_url: "https://placehold.co/200x200/D8C9A8/6B4A2B?text=Bosch+%E2%9A%A0",
|
||
rating_max: 4.7, reviews_total: 1240, stores_count: 2,
|
||
ozon: { url: "https://www.ozon.ru/product/bosch-pue611bb5e/", price_min_rub: 64990 },
|
||
yamarket: { url: "https://market.yandex.ru/product--bosch-pue611bb5e/", price_min_rub: 78000 }
|
||
}
|
||
}
|
||
]
|
||
}
|
||
},
|
||
total_price_estimate_rub: { min: 280000, max: 420000 },
|
||
budget_status: "в_рамках",
|
||
warnings: [
|
||
"Холодильник Bosch KGN39 имеет глубину 660мм — уточнить нишу клиента (стандарт 600мм)"
|
||
]
|
||
};
|
||
|
||
/* ---- mini-копия renderReport из podbor.js, чтобы превью работало автономно ---- */
|
||
|
||
function el(html) {
|
||
const t = document.createElement("template");
|
||
t.innerHTML = html.trim();
|
||
return t.content.firstChild;
|
||
}
|
||
|
||
function _esc(s) {
|
||
return String(s == null ? "" : s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
function formatRub(n) {
|
||
if (!n) return "—";
|
||
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||
}
|
||
|
||
function renderReport(ai, leadId) {
|
||
const summary = ai.summary || "";
|
||
const byCat = ai.by_category || {};
|
||
const total = ai.total_price_estimate_rub || {};
|
||
const budgetStatus = ai.budget_status || "";
|
||
const warnings = ai.warnings || [];
|
||
|
||
const wrap = el(`<section class="report"></section>`);
|
||
|
||
wrap.appendChild(el(`
|
||
<div class="report-head">
|
||
<div class="kicker">Отчёт · ${leadId.slice(0, 8)}</div>
|
||
${summary ? `<p class="report-summary">${_esc(summary)}</p>` : ""}
|
||
</div>
|
||
`));
|
||
|
||
for (const [catKey, catData] of Object.entries(byCat)) {
|
||
const catMeta = (window.PODBOR_CATEGORIES || []).find(c => c.key === catKey);
|
||
const catLabel = catMeta?.label || catKey;
|
||
const catIcon = catMeta?.icon;
|
||
const models = (catData && catData.models) || [];
|
||
if (!models.length) continue;
|
||
|
||
const catAnalysis = (catData && catData.analysis) || "";
|
||
const catNode = el(`
|
||
<div class="report-cat">
|
||
<h3 class="report-cat-head">
|
||
<span class="report-cat-icon">${(catIcon && window.ICONS && window.ICONS[catIcon]) || ""}</span>
|
||
${_esc(catLabel)}
|
||
</h3>
|
||
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
|
||
<div class="report-models"></div>
|
||
</div>
|
||
`);
|
||
const modelsWrap = catNode.querySelector(".report-models");
|
||
|
||
// Матрица цен — primary view, ВСТАВЛЯЕМ ДО списка моделей
|
||
const matrixNode = _renderPriceMatrix(models);
|
||
if (matrixNode) catNode.insertBefore(matrixNode, modelsWrap);
|
||
|
||
for (const m of models) {
|
||
modelsWrap.appendChild(_renderModelCard(m));
|
||
}
|
||
|
||
wrap.appendChild(catNode);
|
||
}
|
||
|
||
if (total && (total.min || total.max)) {
|
||
const tmin = total.min, tmax = total.max;
|
||
const range = (tmin && tmax && tmin !== tmax)
|
||
? `${formatRub(tmin)} — ${formatRub(tmax)} ₽`
|
||
: `${formatRub(tmin || tmax)} ₽`;
|
||
wrap.appendChild(el(`
|
||
<div class="report-total">
|
||
<span class="lbl">ИТОГО</span>
|
||
<strong>${range}</strong>
|
||
${budgetStatus ? `<span class="status">${_esc(budgetStatus)}</span>` : ""}
|
||
</div>
|
||
`));
|
||
}
|
||
|
||
if (warnings.length) {
|
||
const wn = el(`<div class="report-warnings"></div>`);
|
||
warnings.forEach(w => wn.appendChild(el(`<div>⚠️ ${_esc(w)}</div>`)));
|
||
wrap.appendChild(wn);
|
||
}
|
||
|
||
return wrap;
|
||
}
|
||
|
||
function _renderModelCard(m) {
|
||
const enriched = m.enriched || {};
|
||
const pMin = m.price_min_rub || enriched.price_min_rub;
|
||
const pMax = m.price_max_rub || enriched.price_max_rub;
|
||
const img = enriched.image_url;
|
||
const rating = enriched.rating_max;
|
||
const reviews = enriched.reviews_total;
|
||
const stores = enriched.stores_count;
|
||
|
||
const priceHtml = (pMin && pMax && pMin !== pMax)
|
||
? `<strong>${formatRub(pMin)}</strong> — <strong>${formatRub(pMax)}</strong> ₽`
|
||
: pMin ? `<strong>${formatRub(pMin)}</strong> ₽`
|
||
: `<span class="muted">цена уточняется</span>`;
|
||
|
||
const metaParts = [];
|
||
if (rating) metaParts.push(`<span class="rating">★ ${Number(rating).toFixed(1)}</span>`);
|
||
if (reviews) metaParts.push(`<span class="reviews">${reviews} отзыв.</span>`);
|
||
if (stores) metaParts.push(`<span class="stores">${stores} магазинов</span>`);
|
||
|
||
const sourcesData = [
|
||
{ key: "ozon", label: "OZON", item: enriched.ozon },
|
||
{ key: "citilink", label: "Citilink", item: enriched.citilink },
|
||
{ key: "wb", label: "Wildberries", item: enriched.wb },
|
||
{ key: "yamarket", label: "Я.Маркет", item: enriched.yamarket },
|
||
{ key: "dns", label: "DNS", item: enriched.dns },
|
||
];
|
||
const sourceLinks = sourcesData
|
||
.filter(s => s.item && s.item.url)
|
||
.map(s => `<a href="${_esc(s.item.url)}" target="_blank" rel="noopener noreferrer" class="report-link report-link--${s.key}">${s.label}${s.item.price_min_rub ? ` · ${formatRub(s.item.price_min_rub)} ₽` : ""}</a>`);
|
||
|
||
function _plural(n) {
|
||
const last = n % 10, lastTwo = n % 100;
|
||
if (lastTwo >= 11 && lastTwo <= 14) return "магазинов";
|
||
if (last === 1) return "магазин";
|
||
if (last >= 2 && last <= 4) return "магазина";
|
||
return "магазинов";
|
||
}
|
||
|
||
// Specs block
|
||
const specs = m.specs || {};
|
||
const specsItems = [];
|
||
if (specs.dimensions_mm) specsItems.push({ label: "Габариты ШхГxВ", value: specs.dimensions_mm + " мм", highlight: true });
|
||
if (specs.volume_l) specsItems.push({ label: "Объём", value: specs.volume_l + " л" });
|
||
if (specs.weight_kg) specsItems.push({ label: "Вес", value: specs.weight_kg + " кг" });
|
||
if (specs.noise_db) specsItems.push({ label: "Шум", value: specs.noise_db + " дБ" });
|
||
if (specs.energy_class) specsItems.push({ label: "Класс энергии", value: specs.energy_class });
|
||
if (specs.color) specsItems.push({ label: "Цвет / материал", value: specs.color });
|
||
const specsHtml = specsItems.length ? `
|
||
<div class="report-specs">
|
||
<div class="report-specs-head">Характеристики</div>
|
||
<div class="report-specs-grid">
|
||
${specsItems.map(i => `<div class="spec-item${i.highlight ? " highlight" : ""}"><div class="spec-label">${_esc(i.label)}</div><div class="spec-value">${_esc(i.value)}</div></div>`).join("")}
|
||
</div>
|
||
</div>` : "";
|
||
|
||
// Utility links
|
||
const utilButtons = [];
|
||
if (m.manual_search_query) {
|
||
utilButtons.push(`<a href="https://www.google.com/search?q=${encodeURIComponent(m.manual_search_query)}" target="_blank" class="util-link util-link--manual">📄 Инструкция</a>`);
|
||
}
|
||
if (specs.dimensions_mm) {
|
||
utilButtons.push(`<a href="https://www.google.com/search?q=${encodeURIComponent((m.brand || '') + ' ' + (m.model || '') + ' габариты схема установки')}" target="_blank" class="util-link util-link--dims">📐 Схема установки</a>`);
|
||
}
|
||
const utilHtml = utilButtons.length ? `<div class="report-util-links">${utilButtons.join("")}</div>` : "";
|
||
|
||
return el(`
|
||
<article class="report-model">
|
||
<div class="report-model-img${img ? "" : " placeholder"}">${img ? `<img src="${_esc(img)}" alt="" loading="lazy">` : ""}</div>
|
||
<div class="report-model-body">
|
||
<div class="report-model-brand">${_esc(m.brand || "")}</div>
|
||
<div class="report-model-name">${_esc(m.model || "")}</div>
|
||
${metaParts.length ? `<div class="report-model-meta">${metaParts.join(" · ")}</div>` : ""}
|
||
<div class="report-model-price">${priceHtml}</div>
|
||
${(m.highlights || []).length ? `<div class="report-highlights">✓ ${m.highlights.map(_esc).join(" · ")}</div>` : ""}
|
||
|
||
${(m.pros || []).length ? `
|
||
<div class="report-pros-block">
|
||
<div class="pc-head">Плюсы</div>
|
||
<ul class="pc-list">${m.pros.slice(0, 4).map(p => `<li>${_esc(p)}</li>`).join("")}</ul>
|
||
</div>
|
||
` : ""}
|
||
|
||
${(m.cons || []).length ? `
|
||
<div class="report-cons-block">
|
||
<div class="pc-head">Минусы</div>
|
||
<ul class="pc-list">${m.cons.slice(0, 3).map(c => `<li>${_esc(c)}</li>`).join("")}</ul>
|
||
</div>
|
||
` : ""}
|
||
|
||
${specsHtml}
|
||
|
||
${m.reasoning ? `<div class="report-reasoning">💡 ${_esc(m.reasoning)}</div>` : ""}
|
||
|
||
${utilHtml}
|
||
|
||
${sourceLinks.length ? `
|
||
<div class="report-links">
|
||
<div class="report-links-head">${sourceLinks.length} ${_plural(sourceLinks.length)} нашли товар:</div>
|
||
${sourceLinks.join("")}
|
||
</div>
|
||
` : `<div class="report-links-empty">— не найден в подключённых магазинах</div>`}
|
||
</div>
|
||
</article>
|
||
`);
|
||
}
|
||
|
||
function _renderPriceMatrix(models) {
|
||
if (!models || !models.length) return null;
|
||
const STORES = [
|
||
{ key: "ozon", label: "OZON" },
|
||
{ key: "citilink", label: "Citilink" },
|
||
{ key: "wb", label: "Wildberries" },
|
||
{ key: "yamarket", label: "Я.Маркет" },
|
||
{ key: "dns", label: "DNS" },
|
||
];
|
||
const activeStores = STORES.filter(s =>
|
||
models.some(m => (m.enriched || {})[s.key] && ((m.enriched || {})[s.key].price_min_rub || (m.enriched || {})[s.key].url))
|
||
);
|
||
const showStoresCols = activeStores.length > 0;
|
||
|
||
const head = `
|
||
<thead>
|
||
<tr>
|
||
<th class="col-model">Модель</th>
|
||
${showStoresCols
|
||
? activeStores.map(s => `<th class="col-store">${s.label}</th>`).join("")
|
||
: `<th class="col-store">Цена (AI)</th>`}
|
||
<th class="col-best">Мин</th>
|
||
</tr>
|
||
</thead>
|
||
`;
|
||
|
||
const rows = models.map(m => {
|
||
const enriched = m.enriched || {};
|
||
const cellPrices = showStoresCols
|
||
? activeStores.map(s => {
|
||
const item = enriched[s.key];
|
||
return { store: s.key, price: item && item.price_min_rub, url: item && item.url };
|
||
})
|
||
: [{ store: "ai", price: enriched.price_min_rub || m.price_min_rub, url: null }];
|
||
|
||
const validPrices = cellPrices.map(c => c.price).filter(p => p && p > 0);
|
||
const bestPrice = validPrices.length ? Math.min(...validPrices) : null;
|
||
|
||
const modelCell = `
|
||
<td class="col-model">
|
||
<div class="m-brand">${_esc(m.brand || "")}</div>
|
||
<div class="m-name">${_esc(m.model || "")}</div>
|
||
</td>
|
||
`;
|
||
const storeCells = cellPrices.map(c => {
|
||
if (!c.price) {
|
||
return c.url
|
||
? `<td class="cell-price"><a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer" class="cell-noprice-link">смотреть →</a></td>`
|
||
: `<td class="cell-price empty">—</td>`;
|
||
}
|
||
const isBest = bestPrice && c.price === bestPrice;
|
||
const priceHtml = `<strong>${formatRub(c.price)}</strong> ₽`;
|
||
const cellInner = c.url
|
||
? `<a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer">${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}</a>`
|
||
: `${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}`;
|
||
return `<td class="cell-price${isBest ? ' best' : ''}">${cellInner}</td>`;
|
||
}).join("");
|
||
const bestCell = `<td class="col-best">${bestPrice ? `<strong>${formatRub(bestPrice)}</strong>` : "—"}</td>`;
|
||
return `<tr>${modelCell}${storeCells}${bestCell}</tr>`;
|
||
}).join("");
|
||
|
||
return el(`
|
||
<div class="report-matrix-wrap">
|
||
<div class="report-matrix-head">Цены по магазинам — лучшая отмечена ✓</div>
|
||
<div class="report-matrix-scroll">
|
||
<table class="report-matrix">
|
||
${head}
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
function _renderCompareTable(models) {
|
||
const rows = models.map(m => {
|
||
const e = m.enriched || {};
|
||
const p = m.price_min_rub || e.price_min_rub;
|
||
return `
|
||
<tr>
|
||
<td><strong>${_esc(m.brand || "")}</strong> ${_esc(m.model || "")}</td>
|
||
<td>${p ? formatRub(p) + " ₽" : "—"}</td>
|
||
<td>${e.reviews_total || "—"}</td>
|
||
<td>${e.rating_max ? Number(e.rating_max).toFixed(1) : "—"}</td>
|
||
</tr>
|
||
`;
|
||
}).join("");
|
||
|
||
return el(`
|
||
<details class="report-compare" open>
|
||
<summary>Сравнить модели</summary>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Модель</th><th>Цена от</th><th>Отзывов</th><th>★</th></tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</details>
|
||
`);
|
||
}
|
||
|
||
document.getElementById("reportRoot").appendChild(renderReport(MOCK_AI, "ABC12345"));
|
||
</script>
|
||
</body>
|
||
</html>
|