/* ============================================================ Замер — упрощённая версия: только фото + заметки. DWG-чертёж потом делается отдельным процессом из фото. ============================================================ */ const Measurements = (function () { const STORAGE_KEY = "zov-measurement-draft"; // Фото держим только в памяти — data-URL'ы тяжёлые let photos = []; // Array<{ name, dataUrl }> let state = loadState(); let root = null; let measurementId = ""; // если задан — update-mode (закрытие существующей заявки) let prefilledClient = null; // данные клиента из заявки в update-mode function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw); } catch (e) {} return defaultState(); } function defaultState() { return { client_name: "", client_phone: "", address: "", notes: "", }; } function saveState() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } function reset() { state = defaultState(); saveState(); photos = []; prefilledClient = null; } /* ===================== Mount + Render ===================== */ function mount(container) { root = container; document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); photos = []; measurementId = ""; prefilledClient = null; // ?id= → update-mode (замерщик закрывает заявку) const hashMatch = (location.hash.split("?")[1] || ""); const fragQp = new URLSearchParams(hashMatch); const mid = fragQp.get("id") || new URLSearchParams(location.search).get("measurement_id") || ""; if (mid) { measurementId = mid; loadRequestAndStart(); return; } render(); } async function loadRequestAndStart() { root.innerHTML = ""; root.appendChild(renderHeader("Закрыть заявку")); root.appendChild(el(`
`)); try { const res = await fetch(`${BACKEND_URL}/api/measurement_detail`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null, measurement_id: measurementId, }), }); const data = await res.json(); if (data.error) { root.innerHTML = ""; root.appendChild(renderHeader("Ошибка")); root.appendChild(el(`
${data.error}
`)); return; } prefilledClient = { name: data.client_name || "", phone: data.client_phone || "", address: data.address || "", }; // Не показываем форму клиента — она read-only state.notes = state.notes || ""; render(); } catch (e) { root.innerHTML = ""; root.appendChild(renderHeader("Ошибка")); root.appendChild(el(`
Сеть: ${e.message}
`)); } } function render() { if (!root) return; root.innerHTML = ""; root.appendChild(renderHeader(measurementId ? "Закрыть заявку" : "Новый замер")); const screen = el(`
`); root.appendChild(screen); screen.appendChild(renderForm()); } function renderHeader(title) { const h = el(`
${escHtml(title)}
`); h.querySelector(".podbor-back").addEventListener("click", () => { location.hash = ""; location.reload(); }); return h; } /* ===================== Главный экран — всё на одной странице ===================== */ function renderForm() { const isUpdate = !!measurementId && prefilledClient; const clientBlock = isUpdate ? renderClientReadOnly() : renderClientInputs(); const node = el(`

${isUpdate ? "Фото
с замера" : "Новый
замер"}

${isUpdate ? "Загрузите фото от руки нарисованных замеров (а также общие фото помещения). Чертёж сделаем по ним отдельно." : "Заполните данные клиента и загрузите фото замера. Можно фотать рукописные эскизы — главное чтобы были видны размеры."}

📷 Фото замера
`); node.querySelector("#clientBlock").appendChild(clientBlock); bindInputs(node); bindPhotoInput(node); node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node)); return node; } function renderClientReadOnly() { return el(`
Клиент ${escHtml(prefilledClient.name || "—")}
${prefilledClient.phone ? `
Телефон ${escHtml(prefilledClient.phone)}
` : ""} ${prefilledClient.address ? `
Адрес ${escHtml(prefilledClient.address)}
` : ""}
`); } function renderClientInputs() { const block = el(`
`); return block; } function bindInputs(node) { node.querySelectorAll("[data-bind]").forEach(inp => { inp.addEventListener("input", e => { state[e.target.dataset.bind] = e.target.value; saveState(); }); }); } function bindPhotoInput(node) { const list = node.querySelector("#photoList"); const input = node.querySelector("#photoInput"); function refreshList() { list.innerHTML = ""; photos.forEach((ph, idx) => { const tile = el(`
фото ${idx + 1}
`); tile.querySelector(".photo-rm").addEventListener("click", e => { const i = +e.currentTarget.dataset.idx; photos.splice(i, 1); haptic && haptic("impact"); refreshList(); }); list.appendChild(tile); }); } input.addEventListener("change", async (e) => { const files = Array.from(e.target.files || []); input.value = ""; if (!files.length) return; for (const f of files) { if (photos.length >= 20) break; if (!f.type || !f.type.startsWith("image/")) continue; try { const dataUrl = await compressImage(f, 1800, 0.78); photos.push({ name: f.name || `photo_${photos.length + 1}`, dataUrl }); } catch (err) { console.warn("Не удалось сжать фото", err); } } refreshList(); haptic && haptic("success"); }); refreshList(); } /* Сжатие через canvas */ function compressImage(file, maxSide = 1800, quality = 0.78) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = reject; reader.onload = e => { const img = new Image(); img.onerror = reject; img.onload = () => { let { width, height } = img; if (width > maxSide || height > maxSide) { if (width >= height) { height = Math.round(height * maxSide / width); width = maxSide; } else { width = Math.round(width * maxSide / height); height = maxSide; } } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; canvas.getContext("2d").drawImage(img, 0, 0, width, height); try { resolve(canvas.toDataURL("image/jpeg", quality)); } catch (err) { reject(err); } }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } /* ===================== Submit ===================== */ async function onSubmit(node) { const btn = node.querySelector("#submitBtn"); const result = node.querySelector("#submitResult"); btn.disabled = true; btn.innerHTML = ' сохраняем...'; result.innerHTML = ""; // Валидация: для новой записи нужны клиент + телефон + хотя бы 1 фото const isUpdate = !!measurementId && prefilledClient; if (!isUpdate) { const name = (state.client_name || "").trim(); const phone = (state.client_phone || "").trim(); const nameErr = node.querySelector("#nameError"); const phoneErr = node.querySelector("#phoneError"); if (nameErr) nameErr.textContent = ""; if (phoneErr) phoneErr.textContent = ""; if (!name) { if (nameErr) nameErr.textContent = "Укажите имя клиента"; btn.disabled = false; btn.textContent = "Сохранить замер"; return; } if (phone.replace(/\D/g, "").length < 10) { if (phoneErr) phoneErr.textContent = "Слишком короткий номер"; btn.disabled = false; btn.textContent = "Сохранить замер"; return; } } if (!photos.length) { result.innerHTML = `
Добавьте хотя бы одно фото замера.
`; btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер"; return; } const measurement = { photos: photos.map(p => p.dataUrl), notes: state.notes || "", client_name: isUpdate ? prefilledClient.name : state.client_name, client_phone: isUpdate ? prefilledClient.phone : state.client_phone, address: isUpdate ? prefilledClient.address : state.address, measurement_id: measurementId || undefined, }; try { const res = await fetch(`${BACKEND_URL}/api/measurement`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null, measurement, }), }); const data = await res.json(); if (data.error) { result.innerHTML = `
Ошибка: ${data.error}
`; btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер"; return; } haptic && haptic("success"); result.innerHTML = `
${ICONS.check}
${isUpdate ? "Заявка закрыта" : "Замер сохранён"}
${photos.length} фото · ID #${(data.id || "").slice(0, 6)}
`; reset(); node.querySelector("#newOne")?.addEventListener("click", () => mount(root)); node.querySelector("#toHome")?.addEventListener("click", () => { location.hash = ""; location.reload(); }); } catch (e) { result.innerHTML = `
Сеть: ${e.message}
`; btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер"; } } /* ===================== Helpers ===================== */ function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function escAttr(s) { return escHtml(s); } return { mount, reset }; })();