/* ============================================================
Подбор техники — цикл согласования (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 = {}, timeoutMs = 15000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
method: "POST",
signal: ctrl.signal,
body: JSON.stringify({ ...authBody(), ...extra }),
});
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
throw e;
} finally {
clearTimeout(timer);
}
}
// ── 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 `${escHtml(lbl)} `;
}).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(`
`));
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 ? `` : ""}
`));
}
}
// ── 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
? ``
: ""}
`);
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 ? `` : ""}
${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(`
+ Добавить позицию
Категория
${MANAGER_CATEGORIES.map(c =>
`${escHtml(c.label)} `
).join("")}
Магазин
—
DNS
Wildberries
Ozon
Ситилинк
Яндекс Маркет
Добавить
`);
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 символов
🤖 Анализировать
`));
// 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(`
`));
}
// 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(`
`));
}
// Recommendations
const recs = analysis.recommendations || [];
if (recs.length) {
wrap.appendChild(el(`
✅ Рекомендации
${recs.map(r => `${escHtml(r)} `).join("")}
`));
}
// Missing clauses
const missing = analysis.missing_clauses || [];
if (missing.length) {
wrap.appendChild(el(`
❓ Чего нет в договоре
${missing.map(m => `${escHtml(m)} `).join("")}
`));
}
// Footer note
wrap.appendChild(el(`
`));
return wrap;
}
// ══════════════════════════════════════════════════════════
// PUBLIC API
// ══════════════════════════════════════════════════════════
return { mountClient, mountManager, mountContractReview };
})();