/* ============================================================ Замер — структурированная загрузка фото по типам. Типы фото: стена 1-4, план комнаты, общий вид, деталь. ============================================================ */ const Measurements = (function () { const STORAGE_KEY = "zov-measurement-draft-v2"; // Типы фото — в соответствии с чек-листом ЗАМЕРОВ const PHOTO_KINDS = [ { key: "wall1", label: "Стена 1" }, { key: "wall2", label: "Стена 2" }, { key: "wall3", label: "Стена 3" }, { key: "wall4", label: "Стена 4" }, { key: "plan", label: "План комнаты" }, { key: "general", label: "Общий вид" }, { key: "detail", label: "Деталь" }, ]; function kindLabel(k) { return (PHOTO_KINDS.find(p => p.key === k) || {}).label || k; } // Фото держим только в памяти let photos = []; // Array<{ dataUrl, kind }> let state = loadState(); let root = null; let measurementId = ""; // если задан — update-mode (закрытие заявки) let prefilledClient = null; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return { ...defaultState(), ...JSON.parse(raw) }; } catch (e) {} return defaultState(); } function defaultState() { const todayStr = new Date().toISOString().slice(0, 10); return { client_name: "", client_phone: "", address: "", notes: "", // Общая инфа замера (по чек-листу) zamer_no: "", zamer_date: todayStr, floor_base: "0,000 = +88 мм над плитой", }; } 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; const hashMatch = (location.hash.split("?")[1] || ""); const fragQp = new URLSearchParams(hashMatch); const mid = fragQp.get("id") || new URLSearchParams(location.search).get("measurement_id") || ""; // Спецроут #/measure/checklist — показать чек-лист if (location.hash.startsWith("#/measure/checklist")) { renderChecklist(); return; } 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 || "", }; 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(); }); h.querySelector("#openChecklist").addEventListener("click", () => { location.hash = "#/measure/checklist"; }); 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("#openChecklist2").addEventListener("click", () => { location.hash = "#/measure/checklist"; }); 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() { return el(`
`); } function bindInputs(node) { node.querySelectorAll("[data-bind]").forEach(inp => { inp.addEventListener("input", e => { state[e.target.dataset.bind] = e.target.value; saveState(); }); }); } function nextKindSuggestion() { // Авто-предложение: сначала Стена 1, потом 2,3,4, затем План, затем Общий const usedWalls = new Set(photos.filter(p => p.kind?.startsWith("wall")).map(p => p.kind)); for (let i = 1; i <= 4; i++) { if (!usedWalls.has(`wall${i}`)) return `wall${i}`; } const hasPlan = photos.some(p => p.kind === "plan"); if (!hasPlan) return "plan"; return "general"; } function bindPhotoInput(node) { const list = node.querySelector("#photoList"); const input = node.querySelector("#photoInput"); function refreshList() { list.innerHTML = ""; if (!photos.length) { list.innerHTML = `
Ещё нет фото
`; return; } 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(); }); tile.querySelector(".photo-kind").addEventListener("change", e => { const i = +e.target.dataset.idx; photos[i].kind = e.target.value; }); 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 >= 30) break; if (!f.type || !f.type.startsWith("image/")) continue; try { const dataUrl = await compressImage(f, 1800, 0.78); const kind = nextKindSuggestion(); photos.push({ dataUrl, kind }); } catch (err) { console.warn("Не удалось сжать фото", err); } } refreshList(); haptic && haptic("success"); }); refreshList(); } 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); }); } /* ===================== Чек-лист — отдельный экран ===================== */ async function renderChecklist() { root.innerHTML = ""; root.appendChild(el(`
Чек-лист замера
`)); root.querySelector(".podbor-back").addEventListener("click", () => { // Возврат к мастеру (если был открыт через #/measure?id=X) if (measurementId) location.hash = `#/measure?id=${measurementId}`; else location.hash = "#/measure"; }); const wrap = el(`
`); root.appendChild(wrap); wrap.appendChild(el(`
`)); try { const res = await fetch("./assets/zamer-checklist.md", { cache: "no-cache" }); const md = await res.text(); wrap.innerHTML = `
${renderMarkdown(md)}
`; } catch (e) { wrap.innerHTML = `
Не удалось загрузить чек-лист: ${e.message}
`; } } /* Минимальный markdown → HTML: заголовки, списки, таблицы, code */ function renderMarkdown(md) { const lines = md.split("\n"); const out = []; let inList = false; let inTable = false; let tableRows = []; function closeList() { if (inList) { out.push(""); inList = false; } } function closeTable() { if (!inTable) return; if (tableRows.length) { const html = [""]; tableRows.forEach((cells, i) => { const tag = i === 0 ? "th" : "td"; if (i === 1 && cells.every(c => /^[-:\s|]+$/.test(c))) return; // skip separator html.push(`${cells.map(c => `<${tag}>${inline(c)}`).join("")}`); }); html.push("
"); out.push(html.join("")); } tableRows = []; inTable = false; } function inline(s) { return escHtml(s) .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); } for (const raw of lines) { const line = raw.trimEnd(); // Таблица if (line.includes("|") && line.match(/^\s*\|/)) { if (!inTable) { closeList(); inTable = true; } const cells = line.split("|").slice(1, -1).map(s => s.trim()); tableRows.push(cells); continue; } else if (inTable) { closeTable(); } // Заголовки if (line.startsWith("# ")) { closeList(); out.push(`

${inline(line.slice(2))}

`); } else if (line.startsWith("## ")) { closeList(); out.push(`

${inline(line.slice(3))}

`); } else if (line.startsWith("### ")) { closeList(); out.push(`

${inline(line.slice(4))}

`); } else if (line.startsWith("- ") || line.startsWith("* ")) { if (!inList) { out.push("