/* ============================================================ Подбор техники — цикл согласования (Proposals) Клиент: brief → просмотр вариантов → голосование Менеджер: создание → добавление вариантов → отправка ============================================================ */ const Proposals = (function () { // ── Internal helpers ────────────────────────────────────── function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escAttr(s) { return escHtml(s); } function authBody() { return { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null }; } async function apiFetch(path, extra = {}) { const res = await fetch(`${BACKEND_URL}/api/${path}`, { method: "POST", body: JSON.stringify({ ...authBody(), ...extra }), }); if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); } // ── Constants ───────────────────────────────────────────── const CAT_LABELS = { hob: "Варочная панель", oven: "Духовой шкаф", dishwasher: "Посудомойка", hood: "Вытяжка", fridge: "Холодильник", microwave: "Микроволновка", other: "Другое", }; const STATUS_LABELS = { brief: "Анкета принята", draft: "Подборка готовится", sent: "Ожидает вашего ответа", reviewed: "Ответ отправлен", done: "Завершено", }; const STATUS_LABELS_MGR = { brief: "📋 Анкета клиента", draft: "✏️ Черновик", sent: "📨 Отправлено клиенту", reviewed: "📬 Клиент ответил", done: "✅ Завершено", }; const MANAGER_CATEGORIES = [ { key: "hob", label: "Варочная панель" }, { key: "oven", label: "Духовой шкаф" }, { key: "dishwasher", label: "Посудомойка" }, { key: "hood", label: "Вытяжка" }, { key: "fridge", label: "Холодильник" }, { key: "microwave", label: "Микроволновка" }, { key: "other", label: "Другое" }, ]; // ── Plural helper ───────────────────────────────────────── function pluralVariants(n) { if (n % 10 === 1 && n % 100 !== 11) return "вариант"; if ([2,3,4].includes(n % 10) && ![12,13,14].includes(n % 100)) return "варианта"; return "вариантов"; } // ── Radio chips ─────────────────────────────────────────── function radioChips(name, opts, selected) { return opts.map(o => { const [val, lbl] = o.split(":"); return ``; }).join(""); } function setupRadioChips(container) { container.querySelectorAll(".prop-chip").forEach(btn => { btn.addEventListener("click", () => { const name = btn.dataset.name; container.querySelectorAll(`.prop-chip[data-name="${name}"]`).forEach(b => b.classList.remove("on")); btn.classList.add("on"); haptic && haptic("selection"); }); }); } function getRadio(container, name) { const active = container.querySelector(`.prop-chip[data-name="${name}"].on`); return active ? active.dataset.val : null; } // ── Brief summary renderer (for manager view) ───────────── function renderBriefSummary(brief) { const hobMap = { induction: "Индукция", gas: "Газ", electric: "Электро", none: "Не нужна" }; const hoodMap = { builtin: "Встройка", dome: "Купол", none: "Не нужна" }; const rows = []; if (brief.hob) rows.push(["Варочная", hobMap[brief.hob] || brief.hob]); if (brief.oven === "yes") rows.push(["Духовка", "Нужна"]); if (brief.dishwasher && brief.dishwasher !== "none") rows.push(["Посудомойка", brief.dishwasher + " см"]); if (brief.hood && brief.hood !== "none") rows.push(["Вытяжка", hoodMap[brief.hood] || brief.hood]); if (brief.fridge === "yes") rows.push(["Холодильник", "Нужен"]); if (brief.microwave === "yes") rows.push(["Микроволновка", "Нужна"]); if (brief.budget) rows.push(["Бюджет", Number(brief.budget).toLocaleString("ru-RU") + " ₽"]); if (brief.notes) rows.push(["Пожелания", brief.notes]); if (!rows.length) return `

Анкета пустая

`; return `
${rows.map(([k, v]) => `
${escHtml(k)}${escHtml(String(v))}
` ).join("")}
`; } // ── Votes summary for manager ───────────────────────────── function renderVotesSummary(positions) { const yes = [], no = []; for (const cat of (positions || [])) { for (const v of (cat.variants || [])) { const label = `${cat.label || CAT_LABELS[cat.category] || cat.category}: ${v.model || "—"}`; if (v.client_vote === "yes") yes.push(label); else if (v.client_vote === "no") no.push(label); } } if (!yes.length && !no.length) return `

Клиент ещё не голосовал

`; let html = ""; if (yes.length) html += `
✅ Нравится (${yes.length})
${yes.map(l => `
${escHtml(l)}
`).join("")}
`; if (no.length) html += `
❌ Не подходит (${no.length})
${no.map(l => `
${escHtml(l)}
`).join("")}
`; return html; } // ── Source badge ────────────────────────────────────────── const SOURCE_LABELS = { dns: "DNS", wb: "WB", ozon: "Ozon", citilink: "Ситилинк", yamarket: "Яндекс" }; function sourceBadge(src) { if (!src) return ""; return `${escHtml(SOURCE_LABELS[src] || src.toUpperCase())}`; } // ══════════════════════════════════════════════════════════ // CLIENT FLOW // ══════════════════════════════════════════════════════════ async function mountClient(container) { container.innerHTML = `
`; try { const data = await apiFetch("proposal_list"); const proposals = (data.proposals || []); const active = proposals.find(p => ["brief", "draft", "sent", "reviewed"].includes(p.status) ); if (!active) { showClientBriefForm(container, null); return; } if (active.status === "brief" || active.status === "draft") { showClientWaiting(container, active); return; } // sent or reviewed — load full detail const detail = await apiFetch("proposal_detail", { proposal_id: active.id }); if (detail.ok) { showClientProposal(container, detail.proposal); } else { showClientWaiting(container, active); } } catch (e) { container.innerHTML = `
Не удалось загрузить: ${escHtml(e.message)}
`; } } // ── Client: brief form ──────────────────────────────────── function showClientBriefForm(container, prefill) { const p = prefill || {}; container.innerHTML = ""; container.appendChild(el(`
ПОДБОР ТЕХНИКИ
`)); container.querySelector(".podbor-back").addEventListener("click", () => { history.back(); }); const form = el(`

Расскажите,
что нужно?

Ответьте — менеджер подберёт технику под ваш бюджет и кухню.

Варочная панель
${radioChips("hob", ["none:Не нужна","induction:Индукция","gas:Газ","electric:Электро"], p.hob || "none")}
Духовой шкаф
${radioChips("oven", ["no:Не нужен","yes:Нужен"], p.oven || "no")}
Посудомойка
${radioChips("dw", ["none:Не нужна","45:45 см","60:60 см"], p.dishwasher || "none")}
Вытяжка
${radioChips("hood", ["none:Не нужна","builtin:Встройка","dome:Купол"], p.hood || "none")}
Холодильник
${radioChips("fridge_need", ["no:Не нужен","yes:Нужен"], p.fridge || "no")}
Микроволновка
${radioChips("micro_need", ["no:Не нужна","yes:Нужна"], p.microwave || "no")}
Бюджет на технику
Пожелания
`); container.appendChild(form); setupRadioChips(container); container.querySelector("#bf_submit").addEventListener("click", async () => { const btn = container.querySelector("#bf_submit"); const result = container.querySelector("#bf_result"); btn.disabled = true; btn.innerHTML = `Отправляем…`; try { const data = await apiFetch("proposal_brief", { hob: getRadio(container, "hob") || "none", oven: getRadio(container, "oven") || "no", dishwasher: getRadio(container, "dw") || "none", hood: getRadio(container, "hood") || "none", fridge: getRadio(container, "fridge_need") || "no", microwave: getRadio(container, "micro_need") || "no", budget: container.querySelector("#bf_budget")?.value || "", notes: container.querySelector("#bf_notes")?.value || "", }); if (data.error) { result.innerHTML = `
Ошибка: ${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "Отправить менеджеру"; return; } haptic && haptic("success"); showClientWaiting(container, { status: "brief" }); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "Отправить менеджеру"; } }); } // ── Client: waiting screen ──────────────────────────────── function showClientWaiting(container, proposal) { container.innerHTML = ""; container.appendChild(el(`
📋

Анкета принята!

Менеджер подбирает варианты техники.
Как только подборка будет готова — придёт уведомление в бот.

${escHtml(STATUS_LABELS[proposal?.status] || "В работе")}
`)); container.querySelector("#editBriefBtn")?.addEventListener("click", () => { showClientBriefForm(container, null); }); } // ── Client: proposal view (voting) ─────────────────────── function showClientProposal(container, proposal) { container.innerHTML = ""; const positions = proposal.positions || []; const isReviewed = proposal.status === "reviewed"; container.appendChild(el(`
ПОДБОР ТЕХНИКИ
${escHtml(STATUS_LABELS[proposal.status] || proposal.status)}
`)); if (!positions.length) { container.appendChild(el(`
Вариантов пока нет.
`)); return; } const catsWrap = el(`
`); for (const cat of positions) { catsWrap.appendChild(renderClientCategoryBlock(cat, proposal.id, isReviewed)); } container.appendChild(catsWrap); if (!isReviewed) { const submitSection = el(`

Оставьте
комментарий

Нажмите ✅/❌ на каждый вариант и напишите, что понравилось — или нет.

`); container.appendChild(submitSection); container.querySelector("#cl_submit")?.addEventListener("click", async () => { const btn = container.querySelector("#cl_submit"); const result = container.querySelector("#cl_result"); btn.disabled = true; btn.innerHTML = `Отправляем…`; try { const data = await apiFetch("proposal_client_submit", { proposal_id: proposal.id, comment: container.querySelector("#cl_comment")?.value || "", }); if (data.error) { result.innerHTML = `
Ошибка: ${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "Отправить ответ менеджеру"; return; } haptic && haptic("success"); result.innerHTML = `
Ответ отправлен!
Менеджер получил уведомление
`; submitSection.querySelector("textarea, .podbor-cta-row")?.remove(); const statusChip = container.querySelector(".prop-status-chip"); if (statusChip) { statusChip.textContent = STATUS_LABELS.reviewed; statusChip.className = "prop-status-chip reviewed"; } } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "Отправить ответ менеджеру"; } }); } else { container.appendChild(el(`
✅ Вы уже отправили ответ менеджеру. Ожидайте подтверждения. ${proposal.client_comment ? `
«${escHtml(proposal.client_comment)}»
` : ""}
`)); } } // ── Client: category block ──────────────────────────────── function renderClientCategoryBlock(cat, proposalId, isReviewed) { const label = cat.label || CAT_LABELS[cat.category] || cat.category; const variants = cat.variants || []; const block = el(`
${escHtml(label)} ${variants.length} ${pluralVariants(variants.length)}
`); const varList = block.querySelector(".prop-variants-list"); variants.forEach(v => varList.appendChild(renderClientVariantCard(v, proposalId, cat.category, isReviewed))); return block; } // ── Client: variant card ────────────────────────────────── function renderClientVariantCard(v, proposalId, category, isReviewed) { const priceStr = v.price ? `${Number(v.price).toLocaleString("ru-RU")} ₽` : ""; const vote = v.client_vote; const card = el(`
${v.image_url ? `
` : `
`}
${escHtml(v.model || "—")}
${priceStr ? `
${escHtml(priceStr)}
` : ""} ${sourceBadge(v.source)} ${v.manager_comment ? `
💬 ${escHtml(v.manager_comment)}
` : ""} ${v.url ? `Смотреть →` : ""} ${isReviewed ? `
${vote === "yes" ? "✅ Выбрано" : vote === "no" ? "❌ Отклонено" : "— Без оценки"}
` : `
` }
`); if (!isReviewed) { card.querySelectorAll(".prop-vote-btn").forEach(btn => { btn.addEventListener("click", async () => { const newVote = btn.dataset.vote; const finalVote = (v.client_vote === newVote) ? null : newVote; try { const data = await apiFetch("proposal_vote", { proposal_id: proposalId, category, variant_id: v.id, vote: finalVote, }); if (data.ok) { haptic && haptic("impact"); v.client_vote = finalVote; card.className = `prop-variant-card ${finalVote === "yes" ? "voted-yes" : finalVote === "no" ? "voted-no" : ""}`; card.querySelectorAll(".prop-vote-btn").forEach(b => b.classList.remove("active")); if (finalVote) card.querySelector(`.prop-vote-btn[data-vote="${finalVote}"]`)?.classList.add("active"); } } catch (_) {} }); }); } return card; } // ══════════════════════════════════════════════════════════ // MANAGER FLOW // ══════════════════════════════════════════════════════════ async function mountManager(container, clientKey, clientTgId) { container.innerHTML = `
`; try { const data = await apiFetch("proposal_list"); const proposals = (data.proposals || []).filter(p => p.client_key === clientKey); const active = proposals.find(p => ["brief", "draft", "sent", "reviewed"].includes(p.status) ); if (!active) { renderManagerEmpty(container, clientKey, clientTgId); return; } const detail = await apiFetch("proposal_detail", { proposal_id: active.id }); if (detail.ok) { renderManagerEditor(container, detail.proposal, clientKey); } else { renderManagerEmpty(container, clientKey, clientTgId); } } catch (e) { container.innerHTML = `
Ошибка: ${escHtml(e.message)}
`; } } // ── Manager: empty state ────────────────────────────────── function renderManagerEmpty(container, clientKey, clientTgId) { container.innerHTML = ""; container.appendChild(el(`

Подборки для этого клиента ещё нет.

`)); container.querySelector("#mgrCreate")?.addEventListener("click", async () => { const btn = container.querySelector("#mgrCreate"); const result = container.querySelector("#mgrCreateResult"); btn.disabled = true; btn.innerHTML = `Создаём…`; try { const data = await apiFetch("proposal_create", { client_key: clientKey, client_tg_id: clientTgId || "", }); if (data.error) { result.innerHTML = `
${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "Создать подборку"; return; } await mountManager(container, clientKey, clientTgId); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "Создать подборку"; } }); } // ── Manager: main editor ────────────────────────────────── function renderManagerEditor(container, proposal, clientKey) { container.innerHTML = ""; const positions = proposal.positions || []; const canEdit = ["brief", "draft", "reviewed"].includes(proposal.status); const canSend = proposal.status === "draft" && positions.some(p => p.variants?.length); const statusLbl = STATUS_LABELS_MGR[proposal.status] || proposal.status; // Status bar container.appendChild(el(`
${escHtml(statusLbl)} ${proposal.sent_at ? `Отправлено: ${escHtml(proposal.sent_at.slice(0,10))}` : ""} ${proposal.reviewed_at ? `Ответ: ${escHtml(proposal.reviewed_at.slice(0,10))}` : ""}
`)); // Client feedback (reviewed state) if (proposal.status === "reviewed") { const fb = el(`
📬 Ответ клиента
${renderVotesSummary(positions)} ${proposal.client_comment ? `
💬 ${escHtml(proposal.client_comment)}
` : ""}
`); container.appendChild(fb); } // Brief summary (collapsible) if (proposal.brief && Object.keys(proposal.brief).some(k => proposal.brief[k] && proposal.brief[k] !== "none" && proposal.brief[k] !== "no")) { const det = el(`
📋 Анкета клиента
${renderBriefSummary(proposal.brief)}
`); container.appendChild(det); } // Categories if (positions.length) { const catsWrap = el(`
`); positions.forEach(cat => { catsWrap.appendChild(renderManagerCategoryBlock(cat, proposal, canEdit, () => mountManager(container, clientKey, proposal.client_tg_id) )); }); container.appendChild(catsWrap); } else { container.appendChild(el(`
Категорий пока нет. Добавьте первую позицию ниже.
`)); } // Add variant form if (canEdit) { container.appendChild( renderAddVariantForm(proposal.id, clientKey, proposal.client_tg_id, container) ); } // Send button if (canSend) { const sendWrap = el(`
`); container.appendChild(sendWrap); container.querySelector("#mgrSend")?.addEventListener("click", async () => { const btn = container.querySelector("#mgrSend"); const result = container.querySelector("#mgrSendResult"); btn.disabled = true; btn.innerHTML = `Отправляем…`; try { const data = await apiFetch("proposal_send", { proposal_id: proposal.id }); if (data.error) { result.innerHTML = `
${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "📨 Отправить клиенту"; return; } haptic && haptic("success"); await mountManager(container, clientKey, proposal.client_tg_id); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "📨 Отправить клиенту"; } }); } } // ── Manager: category block ─────────────────────────────── function renderManagerCategoryBlock(cat, proposal, canEdit, onRefresh) { const label = cat.label || CAT_LABELS[cat.category] || cat.category; const variants = cat.variants || []; const block = el(`
${escHtml(label)} ${variants.length} ${pluralVariants(variants.length)} ${canEdit ? `` : ""}
`); if (canEdit) { block.querySelector(".prop-cat-del-btn")?.addEventListener("click", async () => { if (!confirm(`Удалить «${label}» со всеми вариантами?`)) return; try { await apiFetch("proposal_remove_variant", { proposal_id: proposal.id, category: cat.category }); await onRefresh(); } catch (_) {} }); } const varWrap = block.querySelector(".prop-mgr-variants"); variants.forEach(v => varWrap.appendChild( renderManagerVariantRow(v, proposal, cat.category, canEdit, onRefresh) )); return block; } // ── Manager: variant row ────────────────────────────────── function renderManagerVariantRow(v, proposal, category, canEdit, onRefresh) { const priceStr = v.price ? `${Number(v.price).toLocaleString("ru-RU")} ₽` : ""; const voteIcon = v.client_vote === "yes" ? " ✅" : v.client_vote === "no" ? " ❌" : ""; const row = el(`
${escHtml(v.model || "—")}${voteIcon}
${priceStr ? `${escHtml(priceStr)}` : ""} ${sourceBadge(v.source)}
${v.manager_comment ? `
${escHtml(v.manager_comment)}
` : ""} ${v.url ? `Открыть →` : ""} ${canEdit ? `` : ""}
`); if (canEdit) { row.querySelector(".prop-variant-del-btn")?.addEventListener("click", async () => { try { await apiFetch("proposal_remove_variant", { proposal_id: proposal.id, category, variant_id: v.id, }); await onRefresh(); } catch (_) {} }); } return row; } // ── Manager: add variant form ───────────────────────────── function renderAddVariantForm(proposalId, clientKey, clientTgId, container) { const wrap = el(`
+ Добавить позицию
Категория
Модель *
Ссылка на товар
Цена, ₽
Магазин
Комментарий
`); wrap.querySelector("#av_save")?.addEventListener("click", async () => { const btn = wrap.querySelector("#av_save"); const result = wrap.querySelector("#av_result"); const model = (wrap.querySelector("#av_model")?.value || "").trim(); if (!model) { result.innerHTML = `
Укажите название модели
`; return; } btn.disabled = true; btn.innerHTML = `Добавляем…`; const catKey = wrap.querySelector("#av_cat")?.value || ""; const catLabel = MANAGER_CATEGORIES.find(c => c.key === catKey)?.label || catKey; try { const data = await apiFetch("proposal_upsert_variant", { proposal_id: proposalId, category: catKey, category_label: catLabel, variant: { model, url: (wrap.querySelector("#av_url")?.value || "").trim(), price: wrap.querySelector("#av_price")?.value || "", source: wrap.querySelector("#av_source")?.value || "", manager_comment: (wrap.querySelector("#av_mgr_comment")?.value || "").trim(), }, }); if (data.error) { result.innerHTML = `
${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "Добавить"; return; } haptic && haptic("success"); // Clear fields ["av_model", "av_url", "av_price", "av_mgr_comment"].forEach(id => { const el2 = wrap.querySelector(`#${id}`); if (el2) el2.value = ""; }); result.innerHTML = `
Добавлено!
`; btn.disabled = false; btn.textContent = "Добавить"; // Reload manager view await mountManager(container, clientKey, clientTgId); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "Добавить"; } }); return wrap; } // ══════════════════════════════════════════════════════════ // CONTRACT REVIEW (AI-анализ договора для клиента) // ══════════════════════════════════════════════════════════ // Preset questions that appear as quick-tap chips const CONTRACT_PRESETS = [ "Какие условия оплаты?", "Когда доставка и монтаж?", "Что будет если я откажусь?", "Есть ли штрафы?", "На что обратить внимание?", ]; async function mountContractReview(container) { container.innerHTML = ""; container.appendChild(el(`
ПРОВЕРКА ДОГОВОРА
`)); container.querySelector(".podbor-back").addEventListener("click", () => history.back()); container.appendChild(el(`

Проверим
ваш договор

Вставьте текст договора — AI объяснит условия простым языком, найдёт риски и подскажет, что уточнить.

Текст договора *
0 / 16 000 символов
Конкретный вопрос (необязательно)
${CONTRACT_PRESETS.map(q => `` ).join("")}
`)); // Char counter const textarea = container.querySelector("#cr_text"); const charEl = container.querySelector("#cr_chars"); textarea.addEventListener("input", () => { const n = textarea.value.length; charEl.textContent = `${n.toLocaleString("ru-RU")} / 16 000 символов`; charEl.style.color = n > 14000 ? "#C0392B" : "var(--muted)"; }); // Preset chips → fill question input container.querySelectorAll(".cr-preset").forEach(btn => { btn.addEventListener("click", () => { container.querySelector("#cr_question").value = btn.textContent.trim(); container.querySelectorAll(".cr-preset").forEach(b => b.classList.remove("on")); btn.classList.add("on"); haptic && haptic("selection"); }); }); // Submit container.querySelector("#cr_submit").addEventListener("click", async () => { const btn = container.querySelector("#cr_submit"); const result = container.querySelector("#cr_result"); const text = textarea.value.trim(); const question = (container.querySelector("#cr_question")?.value || "").trim(); if (!text) { result.innerHTML = `
Вставьте текст договора
`; return; } btn.disabled = true; btn.innerHTML = `Анализируем…`; result.innerHTML = `
🤔
AI читает договор…
Обычно занимает 10–25 секунд
`; try { const data = await apiFetch("contract_review", { text, question }); if (data.error) { result.innerHTML = `
Ошибка: ${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "🤖 Анализировать"; return; } haptic && haptic("success"); result.innerHTML = ""; result.appendChild(renderContractAnalysis(data.analysis, data.raw_text, question)); btn.disabled = false; btn.textContent = "🤖 Анализировать снова"; // Scroll to result result.scrollIntoView({ behavior: "smooth", block: "start" }); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "🤖 Анализировать"; } }); } function renderContractAnalysis(analysis, rawText, question) { // If AI returned unstructured text (not JSON), show it plain if (!analysis || !Object.keys(analysis).length) { return el(`
${escHtml(rawText || "AI не вернул анализ")}
`); } const wrap = el(`
`); // Summary if (analysis.summary) { wrap.appendChild(el(`
📋 Резюме
${escHtml(analysis.summary)}
`)); } // Question answer (if specific question asked) if (question && analysis.question_answer) { wrap.appendChild(el(`
💬 ${escHtml(question)}
${escHtml(analysis.question_answer)}
`)); } // Payment const pay = analysis.payment; if (pay && (pay.total || pay.schedule)) { const rows = []; if (pay.total) rows.push(["Итого", pay.total]); if (pay.schedule) rows.push(["Схема оплаты", pay.schedule]); if (pay.prepayment_pct != null) rows.push(["Предоплата", `${pay.prepayment_pct}%`]); wrap.appendChild(el(`
💰 Оплата
${rows.map(([k, v]) => `
${escHtml(k)} ${escHtml(String(v))}
`).join("")}
`)); } // Deadlines const deadlines = analysis.deadlines || []; if (deadlines.length) { const rows = deadlines.map(d => `
${escHtml(d.label || "")} ${escHtml(d.value || "—")} ${d.note ? `${escHtml(d.note)}` : ""}
`).join(""); wrap.appendChild(el(`
⏰ Сроки
${rows}
`)); } // Risks const risks = analysis.risks || []; if (risks.length) { const riskItems = risks.map(r => { const cls = r.level === "high" ? "high" : r.level === "medium" ? "medium" : "low"; const icon = r.level === "high" ? "🔴" : r.level === "medium" ? "🟡" : "🟢"; return `
${icon} ${escHtml(r.title || "")}
${escHtml(r.description || "")}
`; }).join(""); wrap.appendChild(el(`
⚠️ Риски
${riskItems}
`)); } // Recommendations const recs = analysis.recommendations || []; if (recs.length) { wrap.appendChild(el(`
✅ Рекомендации
`)); } // Missing clauses const missing = analysis.missing_clauses || []; if (missing.length) { wrap.appendChild(el(`
❓ Чего нет в договоре
`)); } // Footer note wrap.appendChild(el(` `)); return wrap; } // ══════════════════════════════════════════════════════════ // PUBLIC API // ══════════════════════════════════════════════════════════ return { mountClient, mountManager, mountContractReview }; })();