/* ============================================================ Клиенты — список + история подборов ============================================================ */ const Clients = (function () { let root = null; let clientsCache = null; /* ===================== Mount ===================== */ function mount(container) { root = container; document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); const sub = location.hash.replace(/^#\/clients\/?/, ""); if (sub.startsWith("lead/")) { const leadId = sub.slice(5); renderLead(leadId); } else if (sub.startsWith("client/")) { const clientKey = decodeURIComponent(sub.slice(7)); renderClientHistory(clientKey); } else { renderList(); } } /* ===================== Список клиентов ===================== */ async function renderList() { root.innerHTML = ""; root.appendChild(headerEl("Клиенты", null)); const loading = el(`
`); root.appendChild(loading); let data; try { data = await fetchClients(); clientsCache = data; } catch (e) { loading.remove(); root.appendChild(el(`
Не удалось загрузить: ${e.message}
`)); return; } loading.remove(); if (!data.clients || !data.clients.length) { root.appendChild(el(`

У тебя пока нет подборов с клиентами.
Сделай первый — в кабинете «Подбор техники».

`)); return; } const meta = el(`
${data.count} ${pluralize(data.count, "клиент", "клиента", "клиентов")} · ${countLeads(data.clients)} ${pluralize(countLeads(data.clients), "подбор", "подбора", "подборов")}
`); root.appendChild(meta); const list = el(`
`); for (const c of data.clients) { list.appendChild(renderClientCard(c)); } root.appendChild(list); } function renderClientCard(c) { const lastAt = formatDate(c.last_lead_at); const card = el(`
${initial(c.client_name)}
${escHtml(c.client_name || "Без имени")}
${c.client_phone ? `
${escHtml(c.client_phone)}
` : ""}
${ICONS.chevron || "›"}
`); card.addEventListener("click", () => { haptic && haptic("impact"); const key = c.client_tg_id || c.client_name.toLowerCase(); location.hash = `#/clients/client/${encodeURIComponent(key)}`; }); return card; } /* ===================== История клиента ===================== */ async function renderClientHistory(clientKey) { root.innerHTML = ""; root.appendChild(headerEl("История подборов", "#/clients")); // Берём из кеша если есть let clients = clientsCache?.clients; if (!clients) { try { const data = await fetchClients(); clients = data.clients; clientsCache = data; } catch (e) { root.appendChild(el(`
${e.message}
`)); return; } } const client = clients.find(c => (c.client_tg_id && c.client_tg_id === clientKey) || (c.client_name && c.client_name.toLowerCase() === clientKey) ); if (!client) { root.appendChild(el(`
Клиент не найден
`)); return; } root.appendChild(el(`
${initial(client.client_name)}

${escHtml(client.client_name)}

${client.client_phone ? `
${escHtml(client.client_phone)}
` : ""}
`)); root.appendChild(el(`
Подборы · ${client.leads_count}
`)); const leadsList = el(`
`); for (const lead of client.leads) { const item = el(` `); item.addEventListener("click", () => { haptic && haptic("impact"); location.hash = `#/clients/lead/${lead.id}`; }); leadsList.appendChild(item); } root.appendChild(leadsList); // Замеры этого клиента (если есть) try { const ms = await fetchMeasurements({ client_tg_id: client.client_tg_id || "" }); const myMeasurements = (ms.measurements || []).filter(m => { // Если client_tg_id зарегистрирован — фильтруем по нему if (client.client_tg_id) return String(m.client_tg_id) === String(client.client_tg_id); // Иначе — ищем имя клиента в notes (упрощённая логика для новых клиентов) return (m.notes || "").toLowerCase().includes((client.client_name || "").toLowerCase()); }); if (myMeasurements.length) { root.appendChild(el(`
Замеры · ${myMeasurements.length}
`)); const mList = el(`
`); for (const m of myMeasurements) { const item = el(`
${formatDate(m.created_at)}
${escHtml(layoutLabel(m.layout))}
${m.area_m2 ? m.area_m2 + " м²" : "—"}
`); mList.appendChild(item); } root.appendChild(mList); } } catch (e) { // Игнорируем — секция замеров просто не покажется } } function layoutLabel(key) { return ({ linear: "Прямая", l_shape: "Угловая Г", u_shape: "П-образная", island: "С островом", peninsula: "Полуостров", }[key]) || (key || "—"); } /* ===================== Детали лида (re-render отчёта) ===================== */ async function renderLead(leadId) { root.innerHTML = ""; root.appendChild(headerEl("Подбор", "back")); const loading = el(`
`); root.appendChild(loading); let lead; try { lead = await fetchLead(leadId); } catch (e) { loading.remove(); root.appendChild(el(`
${e.message}
`)); return; } loading.remove(); if (lead.error) { root.appendChild(el(`
${lead.error}
`)); return; } // Шапка root.appendChild(el(`
Подбор #${(lead.id || "").slice(0, 8)}

${escHtml(lead.client_name || "Клиент")}

Сохранён ${formatDate(lead.created_at)}

`)); // Рендерим отчёт через Podbor.renderReport если ai-json есть if (lead.ai && typeof window.Podbor?.renderSavedReport === "function") { const reportNode = window.Podbor.renderSavedReport(lead.ai, lead.id); root.appendChild(reportNode); } else if (lead.ai_text) { // Fallback — AI вернул plain text root.appendChild(el(`
AI ответ
${escHtml(lead.ai_text)}
`)); } else { root.appendChild(el(`
Для этого лида нет AI-ответа.
`)); } } /* ===================== Helpers ===================== */ function headerEl(title, backHref) { const h = el(`
${escHtml(title)}
`); h.querySelector(".podbor-back").addEventListener("click", () => { if (backHref === "back") { history.back(); } else if (backHref) { location.hash = backHref; } else { location.hash = ""; location.reload(); } }); return h; } async function fetchClients() { if (!BACKEND_URL) throw new Error("BACKEND_URL не задан"); const res = await fetch(`${BACKEND_URL}/api/clients`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "" }), }); return await res.json(); } async function fetchLead(leadId) { if (!BACKEND_URL) throw new Error("BACKEND_URL не задан"); const res = await fetch(`${BACKEND_URL}/api/lead`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", lead_id: leadId }), }); return await res.json(); } async function fetchMeasurements(filters = {}) { if (!BACKEND_URL) throw new Error("BACKEND_URL не задан"); const res = await fetch(`${BACKEND_URL}/api/measurements`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", ...filters }), }); return await res.json(); } function initial(name) { return ((name || "?").trim()[0] || "?").toUpperCase(); } function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function formatDate(iso) { if (!iso) return "—"; try { const d = new Date(iso); const now = new Date(); const sameDay = d.toDateString() === now.toDateString(); const dd = String(d.getDate()).padStart(2, "0"); const mm = String(d.getMonth() + 1).padStart(2, "0"); const yy = d.getFullYear(); const hh = String(d.getHours()).padStart(2, "0"); const mi = String(d.getMinutes()).padStart(2, "0"); if (sameDay) return `сегодня · ${hh}:${mi}`; return `${dd}.${mm}.${yy}`; } catch (e) { return iso.slice(0, 10); } } function pluralize(n, one, few, many) { const last = n % 10, lastTwo = n % 100; if (lastTwo >= 11 && lastTwo <= 14) return many; if (last === 1) return one; if (last >= 2 && last <= 4) return few; return many; } function countLeads(clients) { return clients.reduce((s, c) => s + (c.leads_count || 0), 0); } function statusLabel(s) { const map = { "new": "Новый", "sent": "Отправлен", "viewed": "Просмотрен", "ordered": "Оформлен", }; return map[s] || s || "—"; } return { mount }; })();