import { api, toast, fmtDate } from "/api/monitoring-tg/static/js/api.js"; import { getVertical, getSection, VERTICAL_META } from "/api/monitoring-tg/static/js/vertical.js"; const V = getVertical(); const section = getSection(); const meta = VERTICAL_META[V]; const state = { offset: 0, limit: 50, channelId: null, q: "", realEstate: "", hrKind: "", hasPhone: false, leadsOnly: false, minConfidence: 0.5, channels: [], autorefresh: false, timer: null, }; function escape(s) { if (s == null) return ""; return String(s).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } function highlight(text, q) { if (!q || !text) return escape(text); const escaped = escape(text); const re = new RegExp(escape(q).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"); return escaped.replace(re, m => `${m}`); } function channelTitle(id) { const c = state.channels.find(c => c.id === id); return c ? (c.title || c.identifier) : `#${id}`; } function fmtSize(bytes) { if (bytes == null) return ""; if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`; return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; } const REAL_ESTATE_LABELS = { sale: "продажа", rent: "аренда", purchase: "покупка" }; const HR_KIND_LABELS = { vacancy: "вакансия", resume: "резюме", contact: "контакт" }; function senderContacts(m) { const contacts = []; if (m && m.post_url) { contacts.push(`📬 Открыть в Telegram`); } if (m && m.sender_username) { const u = m.sender_username.startsWith("@") ? m.sender_username : "@" + m.sender_username; contacts.push(`✉️ ${escape(u)}`); } else if (m && m.sender_name) { contacts.push(`✍️ ${escape(m.sender_name)}`); } const handles = (m && m.extracted && m.extracted.tg_handles) || []; for (const h of handles) { const bare = h.replace(/^@/, ""); contacts.push(`✉️ ${escape(h)}`); } return contacts; } function renderReLead(lead, m) { if (!lead || !lead.is_listing) return ""; const tone = lead.confidence >= 0.7 ? "lead-strong" : lead.confidence >= 0.4 ? "lead-medium" : "lead-weak"; const bits = []; if (lead.kind) bits.push(REAL_ESTATE_LABELS[lead.kind] || lead.kind); if (lead.property_type) bits.push(lead.property_type); if (lead.rooms) bits.push(lead.rooms); if (lead.area_m2) bits.push(`${lead.area_m2} м²`); const priceBit = lead.price_text || (lead.price_value != null ? `${lead.price_value.toLocaleString()}${lead.currency ? " " + lead.currency : ""}` : null); if (priceBit) bits.push(priceBit); else if (lead.currency) bits.push(lead.currency); if (lead.location) bits.push(lead.location); const facts = bits.length ? `
${escape(bits.join(" · "))}
` : ""; const summary = lead.summary ? `
${escape(lead.summary)}
` : ""; const contacts = []; if (lead.contact_phone) { contacts.push(`📞 ${escape(lead.contact_phone)}`); } if (lead.contact_name) { contacts.push(`👤 ${escape(lead.contact_name)}`); } contacts.push(...senderContacts(m)); return `
🎯 ЛИД · 🏠 ${facts} ${(lead.confidence * 100).toFixed(0)}%
${summary} ${contacts.length ? `
${contacts.join(" ")}
` : ""}
`; } function renderHrLead(lead, m) { if (!lead || !lead.is_lead) return ""; const tone = lead.confidence >= 0.7 ? "lead-strong" : lead.confidence >= 0.4 ? "lead-medium" : "lead-weak"; const bits = []; if (lead.kind) bits.push(HR_KIND_LABELS[lead.kind] || lead.kind); if (lead.title) bits.push(lead.title); if (lead.company) bits.push(lead.company); if (lead.candidate_name) bits.push(lead.candidate_name); if (lead.experience_years != null) bits.push(`${lead.experience_years}+ лет опыта`); if (lead.employment_type) bits.push(lead.employment_type); if (lead.remote === true) bits.push("удалёнка"); else if (lead.remote === false) bits.push("офис"); if (lead.location) bits.push(lead.location); const salaryBit = lead.salary_text || (lead.salary_value != null ? `${lead.salary_value.toLocaleString()}${lead.currency ? " " + lead.currency : ""}` : null); if (salaryBit) bits.push(salaryBit); else if (lead.currency) bits.push(lead.currency); const facts = bits.length ? `
${escape(bits.join(" · "))}
` : ""; const summary = lead.summary ? `
${escape(lead.summary)}
` : ""; const skills = (lead.skills || []).slice(0, 12); const skillsBlock = skills.length ? `
${skills.map(s => `${escape(s)}`).join(" ")}
` : ""; const contacts = []; if (lead.contact_phone) { contacts.push(`📞 ${escape(lead.contact_phone)}`); } if (lead.contact_name) { contacts.push(`👤 ${escape(lead.contact_name)}`); } contacts.push(...senderContacts(m)); return `
🎯 ЛИД · 👥 ${facts} ${(lead.confidence * 100).toFixed(0)}%
${summary} ${skillsBlock} ${contacts.length ? `
${contacts.join(" ")}
` : ""}
`; } function renderExtracted(ex) { if (!ex) return ""; const parts = []; const re = ex.real_estate; const showRegexRE = V === "real_estate" && re && !(ex.lead && ex.lead.is_listing); if (showRegexRE) { const bits = []; if (re.kind) bits.push(REAL_ESTATE_LABELS[re.kind] || re.kind); if (re.property_type) bits.push(re.property_type); if (re.rooms) bits.push(re.rooms); if (re.area_m2) bits.push(`${re.area_m2} м²`); if (re.price) bits.push(re.price); if (bits.length) parts.push(`🏠 regex: ${escape(bits.join(" · "))}`); } // Phones/names from regex are still useful even when there's a lead — show // only those that aren't already inside the lead card. const inLead = new Set(); const activeLead = V === "hr" ? ex.hr_lead : ex.lead; if (activeLead) { if (activeLead.contact_phone) inLead.add(activeLead.contact_phone); if (activeLead.contact_name) inLead.add(activeLead.contact_name); } for (const p of ex.phones || []) { if (inLead.has(p)) continue; parts.push(`📞 ${escape(p)}`); } for (const n of (ex.names || []).slice(0, 3)) { if (inLead.has(n)) continue; parts.push(`👤 ${escape(n)}`); } if ((ex.names || []).length > 3) { parts.push(`+${ex.names.length - 3}`); } const leadShown = (V === "hr" && ex.hr_lead && ex.hr_lead.is_lead) || (V === "real_estate" && ex.lead && ex.lead.is_listing); if (!leadShown) { for (const h of (ex.tg_handles || [])) { const bare = h.replace(/^@/, ""); parts.push(`✉️ ${escape(h)}`); } } const tags = parts.length ? `
${parts.join(" ")}
` : ""; return tags; } function renderMedia(files) { if (!files || !files.length) return ""; return `
${files.map(f => { if (f.skipped) { const why = f.skipped === "too_large" ? "слишком большой" : f.skipped; return `
${escape(f.kind)} ${why}${f.size ? `, ${fmtSize(f.size)}` : ""}
`; } if (!f.url) return ""; if (f.kind === "photo" || f.kind === "sticker") { return ` `; } if (f.kind === "video") { return ``; } if (f.kind === "audio") { return ``; } return ` ${escape(f.kind)} ${escape(f.mime || "файл")} ${fmtSize(f.size)} `; }).join("")}
`; } function readUrl() { const params = new URLSearchParams(location.search); if (params.has("channel_id")) state.channelId = Number(params.get("channel_id")); if (params.has("q")) state.q = params.get("q"); if (params.has("real_estate")) state.realEstate = params.get("real_estate"); if (params.has("hr_kind")) state.hrKind = params.get("hr_kind"); if (params.get("has_phone") === "true") state.hasPhone = true; if (params.get("leads_only") === "true") state.leadsOnly = true; if (params.has("min_confidence")) state.minConfidence = Number(params.get("min_confidence")); } function syncControls() { document.getElementById("channel-filter").value = state.channelId ?? ""; document.getElementById("search").value = state.q; const reSel = document.getElementById("real-estate"); if (reSel) reSel.value = state.realEstate; const hrSel = document.getElementById("hr-kind"); if (hrSel) hrSel.value = state.hrKind; document.getElementById("has-phone").checked = state.hasPhone; document.getElementById("leads-only").checked = state.leadsOnly; document.getElementById("min-confidence").value = String(state.minConfidence); document.getElementById("limit").value = state.limit; } async function loadChannels() { state.channels = await api.listChannels(); const sel = document.getElementById("channel-filter"); sel.innerHTML = `` + state.channels.map(c => `` ).join(""); syncControls(); } async function loadMessages() { const list = document.getElementById("list"); list.innerHTML = `
Загрузка...
`; try { const msgs = await api.listMessages({ channelId: state.channelId, q: state.q || undefined, realEstate: state.realEstate || undefined, hrKind: state.hrKind || undefined, hasPhone: state.hasPhone || undefined, leadsOnly: state.leadsOnly || undefined, minConfidence: state.leadsOnly ? state.minConfidence : undefined, limit: state.limit, offset: state.offset, }); if (!msgs.length) { list.innerHTML = `
Сообщений нет
`; } else { list.innerHTML = msgs.map(m => `
${escape(channelTitle(m.channel_id))} · ${fmtDate(m.date)} · #${m.tg_message_id} ${m.group_size > 1 ? `альбом · ${m.group_size}` : (m.has_media ? 'media' : '')} ${m.views != null ? `👁 ${m.views}` : ''} ${m.forwards ? `↗ ${m.forwards}` : ''}
json
${m.text ? highlight(m.text, state.q) : '(без текста)'}
${V === "hr" ? renderHrLead(m.extracted && m.extracted.hr_lead, m) : renderReLead(m.extracted && m.extracted.lead, m)} ${renderExtracted(m.extracted)} ${renderMedia(m.media_files)}
`).join(""); } document.getElementById("page-info").textContent = `${state.offset + 1}–${state.offset + msgs.length}`; document.getElementById("prev").disabled = state.offset === 0; document.getElementById("next").disabled = msgs.length < state.limit; } catch (err) { toast(err.message, "error"); list.innerHTML = `
Ошибка: ${escape(err.message)}
`; } } document.getElementById("channel-filter").addEventListener("change", (e) => { state.channelId = e.target.value ? Number(e.target.value) : null; state.offset = 0; loadMessages(); }); let searchTimer; document.getElementById("search").addEventListener("input", (e) => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { state.q = e.target.value.trim(); state.offset = 0; loadMessages(); }, 250); }); document.getElementById("limit").addEventListener("change", (e) => { state.limit = Number(e.target.value); state.offset = 0; loadMessages(); }); const reSelEl = document.getElementById("real-estate"); if (reSelEl) { reSelEl.addEventListener("change", (e) => { state.realEstate = e.target.value; state.offset = 0; loadMessages(); }); } const hrSelEl = document.getElementById("hr-kind"); if (hrSelEl) { hrSelEl.addEventListener("change", (e) => { state.hrKind = e.target.value; state.offset = 0; loadMessages(); }); } document.getElementById("has-phone").addEventListener("change", (e) => { state.hasPhone = e.target.checked; state.offset = 0; loadMessages(); }); document.getElementById("leads-only").addEventListener("change", (e) => { state.leadsOnly = e.target.checked; state.offset = 0; loadMessages(); }); document.getElementById("min-confidence").addEventListener("change", (e) => { state.minConfidence = Number(e.target.value); if (state.leadsOnly) { state.offset = 0; loadMessages(); } }); document.getElementById("refresh").addEventListener("click", loadMessages); document.getElementById("prev").addEventListener("click", () => { state.offset = Math.max(0, state.offset - state.limit); loadMessages(); }); document.getElementById("next").addEventListener("click", () => { state.offset += state.limit; loadMessages(); }); document.getElementById("autorefresh").addEventListener("change", (e) => { state.autorefresh = e.target.checked; if (state.timer) { clearInterval(state.timer); state.timer = null; } if (state.autorefresh) state.timer = setInterval(loadMessages, 10000); }); document.getElementById("list").addEventListener("click", async (e) => { const lightbox = e.target.closest("[data-action='lightbox']"); if (lightbox) { e.preventDefault(); openLightbox(lightbox.dataset.url); return; } const a = e.target.closest("[data-action='raw']"); if (!a) return; e.preventDefault(); const id = Number(a.closest(".message").dataset.id); try { const msg = await api.getMessage(id); document.getElementById("raw-content").textContent = JSON.stringify(msg, null, 2); document.getElementById("raw-dialog").showModal(); } catch (err) { toast(err.message, "error"); } }); function openLightbox(url) { let lb = document.getElementById("lightbox"); if (!lb) { lb = document.createElement("div"); lb.id = "lightbox"; lb.addEventListener("click", () => lb.remove()); document.body.appendChild(lb); } lb.innerHTML = ``; } document.getElementById("raw-close").addEventListener("click", () => { document.getElementById("raw-dialog").close(); }); readUrl(); (async () => { await loadChannels(); await loadMessages(); })();