/* ============================================================ Заявка на замер — менеджер создаёт, замерщику в инбокс v20260518p — поиск по клиентам + передача менеджеру ============================================================ */ const MeasurementRequest = (function () { let root = null; let state = { // Клиент client_id: null, // ключ из списка (client_name+phone) — если выбрали client_name: "", client_phone: "", address: "", // Назначение assigned_to_tg_id: "", target_manager_tg_id: "", // Прочее preferred_note: "", urgent: false, }; let allClients = []; // [{client_name, client_phone, address, client_tg_id}] let measurers = []; let managers = []; let clientMode = "search"; // "search" | "selected" | "new" /* ── API ──────────────────────────────────────────────────── */ async function _api(path, body = {}) { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 15000); try { const res = await fetch(`${BACKEND_URL}/api/${path}`, { method: "POST", signal: ctrl.signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ initData: Platform.initData, initDataUnsafe: Platform.initDataUnsafe, ...body, }), }); if (!res.ok) throw new Error(`Ошибка сервера (${res.status})`); return await res.json(); } catch (e) { if (e.name === "AbortError") throw new Error("Сервер не отвечает"); throw e; } finally { clearTimeout(t); } } /* ── Helpers ─────────────────────────────────────────────── */ function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escAttr(s) { return escHtml(s); } function el(html) { const t = document.createElement("template"); t.innerHTML = html.trim(); return t.content.firstChild; } function maskPhone(p) { const d = (p || "").replace(/\D/g, ""); if (d.length < 4) return p; return d.slice(0, 1) + "**" + d.slice(-2); } /* ── Mount ───────────────────────────────────────────────── */ function mount(container) { root = container; document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); _resetState(); // Prefill из sessionStorage (из карточки клиента) try { const raw = sessionStorage.getItem("prefillClient"); if (raw) { const pre = JSON.parse(raw); if (pre.name) state.client_name = pre.name; if (pre.phone) state.client_phone = pre.phone; sessionStorage.removeItem("prefillClient"); clientMode = "new"; // уже знаем имя — режим нового клиента } } catch (e) {} render(); _loadAll(); } function _resetState() { state = { client_id: null, client_name: "", client_phone: "", address: "", assigned_to_tg_id: "", target_manager_tg_id: "", preferred_note: "", urgent: false, }; clientMode = "search"; } /* ── Load: clients + measurers + managers ─────────────────── */ async function _loadAll() { // Параллельно const [cRes, mRes, mgRes] = await Promise.allSettled([ _api("clients"), _api("staff_list", { role: "measurer" }), _api("managers_list"), ]); if (cRes.status === "fulfilled" && !cRes.value.error) { allClients = cRes.value.clients || []; } if (mRes.status === "fulfilled" && !mRes.value.error) { measurers = mRes.value.staff || []; } if (mgRes.status === "fulfilled" && !mgRes.value.error) { managers = mgRes.value.managers || []; } _renderMeasurerSelect(); _renderManagerSelect(); } /* ── Render ──────────────────────────────────────────────── */ function render() { if (!root) return; root.innerHTML = ""; root.appendChild(_headerEl()); const wrap = el(`
`); // ── Заголовок ──────────────────────────────────────────── wrap.appendChild(el(`

Заявка
на замер

Замерщик получит уведомление в Telegram и согласует дату с клиентом.

`)); // ── Блок клиента ───────────────────────────────────────── const clientBlock = el(`
`); wrap.appendChild(clientBlock); _renderClientBlock(clientBlock); // ── Замерщик ───────────────────────────────────────────── const assignBlock = el(`
`); wrap.appendChild(assignBlock); // ── Передать менеджеру ──────────────────────────────────── const mgrBlock = el(`
`); wrap.appendChild(mgrBlock); // ── Примечание ─────────────────────────────────────────── wrap.appendChild(el(`
`)); // ── CTA ─────────────────────────────────────────────────── wrap.appendChild(el(`
`)); root.appendChild(wrap); _bindWrap(wrap); } /* ── Client block ────────────────────────────────────────── */ function _renderClientBlock(container) { container.innerHTML = ""; if (clientMode === "selected") { // Карточка выбранного клиента container.appendChild(el(`
Клиент
${escHtml(state.client_name)}
${escHtml(maskPhone(state.client_phone))}
`)); container.querySelector("#rq-clear-client").addEventListener("click", () => { state.client_id = null; state.client_name = ""; state.client_phone = ""; state.address = ""; clientMode = "search"; _renderClientBlock(container); }); container.querySelector("#rq-address").addEventListener("input", e => { state.address = e.target.value; }); } else if (clientMode === "new") { // Форма нового клиента container.appendChild(el(`
Новый клиент
`)); container.querySelector("#rq-new-name").addEventListener("input", e => { state.client_name = e.target.value; }); container.querySelector("#rq-new-phone").addEventListener("input", e => { state.client_phone = e.target.value; }); container.querySelector("#rq-new-address").addEventListener("input", e => { state.address = e.target.value; }); container.querySelector("#rq-back-search").addEventListener("click", () => { state.client_name = ""; state.client_phone = ""; state.address = ""; clientMode = "search"; _renderClientBlock(container); }); } else { // Режим поиска (default) const searchWrap = el(`
`); container.appendChild(searchWrap); const input = searchWrap.querySelector("#rq-search"); const dropdown = searchWrap.querySelector("#rq-dropdown"); input.addEventListener("input", () => _filterClients(input.value, dropdown, container)); input.addEventListener("focus", () => { if (input.value.trim() || !allClients.length) return; _showDropdown(allClients.slice(0, 6), dropdown, container, input); }); document.addEventListener("click", function _outsideClick(e) { if (!searchWrap.contains(e.target)) { dropdown.style.display = "none"; document.removeEventListener("click", _outsideClick); } }); } } function _filterClients(query, dropdown, container) { const q = query.trim().toLowerCase(); if (!q) { dropdown.style.display = "none"; return; } const matches = allClients.filter(c => (c.client_name || "").toLowerCase().includes(q) || (c.client_phone || "").replace(/\D/g, "").includes(q.replace(/\D/g, "")) ).slice(0, 6); _showDropdown(matches, dropdown, container, document.getElementById("rq-search"), q); } function _showDropdown(list, dropdown, container, input, query = "") { dropdown.innerHTML = ""; dropdown.style.display = ""; list.forEach(c => { const item = el(`
${escHtml(c.client_name || "—")}
${escHtml(maskPhone(c.client_phone))} ${c.address ? " · " + escHtml(c.address.slice(0, 30)) : ""}
`); item.addEventListener("mousedown", e => e.preventDefault()); // не теряем focus item.addEventListener("click", () => { state.client_id = c.client_name + "|" + c.client_phone; state.client_name = c.client_name || ""; state.client_phone = c.client_phone || ""; state.address = c.address || ""; clientMode = "selected"; dropdown.style.display = "none"; _renderClientBlock(container); }); dropdown.appendChild(item); }); // «Создать нового клиента» const newBtn = el(`
Создать нового клиента
`); newBtn.addEventListener("mousedown", e => e.preventDefault()); newBtn.addEventListener("click", () => { if (input) state.client_name = input.value.trim(); clientMode = "new"; dropdown.style.display = "none"; _renderClientBlock(container); }); dropdown.appendChild(newBtn); } /* ── Measurer & Manager selects ──────────────────────────── */ function _renderMeasurerSelect() { const sel = document.getElementById("rq-measurer"); const hint = document.getElementById("rq-measurer-hint"); if (!sel) return; if (!measurers.length) { sel.innerHTML = ``; sel.disabled = true; if (hint) hint.textContent = "Выдайте кому-нибудь роль measurer через /grant_role"; return; } sel.disabled = false; sel.innerHTML = `` + measurers.map(m => `` ).join(""); } function _renderManagerSelect() { const sel = document.getElementById("rq-manager"); if (!sel) return; if (!managers.length) { sel.innerHTML = ``; sel.disabled = true; return; } sel.disabled = false; sel.innerHTML = `` + managers.map(m => `` ).join(""); } /* ── Bind ─────────────────────────────────────────────────── */ function _bindWrap(wrap) { wrap.querySelector("#rq-measurer")?.addEventListener("change", e => { state.assigned_to_tg_id = e.target.value; }); wrap.querySelector("#rq-manager")?.addEventListener("change", e => { state.target_manager_tg_id = e.target.value; }); wrap.querySelector("#rq-note")?.addEventListener("input", e => { state.preferred_note = e.target.value; }); wrap.querySelector("#rq-submit")?.addEventListener("click", () => _onSubmit(wrap)); } /* ── Submit ──────────────────────────────────────────────── */ async function _onSubmit(wrap) { // Валидация const container = wrap.querySelector("#rq-client-block"); const errName = container?.querySelector("#rq-err-name"); const errPhone = container?.querySelector("#rq-err-phone"); if (errName) errName.textContent = ""; if (errPhone) errPhone.textContent = ""; const name = state.client_name.trim(); const phone = state.client_phone.trim(); if (!name) { if (errName) errName.textContent = "Укажите имя клиента"; else Platform.showAlert("Укажите имя клиента"); return; } if (phone.replace(/\D/g, "").length < 10) { if (errPhone) errPhone.textContent = "Слишком короткий номер"; else Platform.showAlert("Укажите телефон клиента"); return; } const btn = wrap.querySelector("#rq-submit"); const result = wrap.querySelector("#rq-result"); btn.disabled = true; btn.innerHTML = ' создаём...'; result.innerHTML = ""; try { const data = await _api("measurement_request", { client_name: name, client_phone: phone, address: state.address || "", assigned_to_tg_id: state.assigned_to_tg_id || "", target_manager_tg_id: state.target_manager_tg_id || "", preferred_note: state.preferred_note || "", preferred_type: "tbd", }); if (data.error) { result.innerHTML = `
Ошибка: ${escHtml(data.error)}
`; btn.disabled = false; btn.textContent = "Попробовать снова"; return; } haptic && haptic("success"); const assignedTo = state.assigned_to_tg_id ? measurers.find(m => String(m.tg_id) === String(state.assigned_to_tg_id)) : null; const handedTo = state.target_manager_tg_id ? managers.find(m => String(m.tg_id) === String(state.target_manager_tg_id)) : null; result.innerHTML = `
Заявка создана
#${(data.id || "").slice(0, 6)} ${assignedTo ? " · Замерщик уведомлён" : ""} ${handedTo ? ` · Передана ${escHtml(handedTo.full_name || "менеджеру")}` : ""}
`; result.querySelector("#rq-new")?.addEventListener("click", () => mount(root)); result.querySelector("#rq-home")?.addEventListener("click", () => { location.hash = ""; if (typeof routeByHash === "function") routeByHash(); }); } catch (e) { result.innerHTML = `
Сеть: ${escHtml(e.message)}
`; btn.disabled = false; btn.textContent = "Попробовать снова"; } } /* ── Header ──────────────────────────────────────────────── */ function _headerEl() { const h = el(`
Новая заявка на замер
`); h.querySelector(".podbor-back").addEventListener("click", () => { location.hash = ""; if (typeof routeByHash === "function") routeByHash(); }); return h; } return { mount }; })();