/* ============================================================
Заявка на замер — менеджер создаёт, замерщику в инбокс
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 };
})();