/* ============================================================ Contracts — предпросмотр и подпись акта сдачи-приёмки №3 mount(container, assemblyId) | route: #/assembly/:id/contract ============================================================ */ const Contracts = (function () { "use strict"; /* ── Утилиты ─────────────────────────────────────────────── */ function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function el(html) { const t = document.createElement("template"); t.innerHTML = html.trim(); return t.content.firstChild; } function fmtMoney(n) { const num = parseFloat(n) || 0; return num.toLocaleString("ru-RU", { minimumFractionDigits: 0 }) + " р."; } function today() { return new Date().toISOString().slice(0, 10); } function fmtDateParts(dateStr) { // "2026-05-19" → { day:"19", month:"мая", year:"2026" } if (!dateStr) { const d = new Date(); dateStr = d.toISOString().slice(0, 10); } const months = [ "января","февраля","марта","апреля","мая","июня", "июля","августа","сентября","октября","ноября","декабря" ]; try { const parts = dateStr.split("-"); return { day: String(parseInt(parts[2])), month: months[parseInt(parts[1]) - 1] || parts[1], year: parts[0], }; } catch { return { day: "—", month: "—", year: "—" }; } } /* ── API ─────────────────────────────────────────────────── */ async function _api(path, body = {}) { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 20000); try { const res = await fetch(`${BACKEND_URL}/api/${path}`, { method: "POST", signal: ctrl.signal, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ initData: (typeof Platform !== "undefined" ? Platform.initData : (window.tg?.initData || "")), initDataUnsafe: (typeof Platform !== "undefined" ? Platform.initDataUnsafe : null), ...body, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (e) { if (e.name === "AbortError") throw new Error("Сервер не отвечает, попробуй ещё раз"); throw e; } finally { clearTimeout(t); } } /* ── Шаблон акта ─────────────────────────────────────────── */ function buildActHtml(fields) { const { contract_num, contract_date, client_name, address, total_sum, assembly_price, travel_spb, travel_outside, tech_list, } = fields; const dp = fmtDateParts(contract_date); const totalFmt = fmtMoney(total_sum); const asmFmt = fmtMoney(assembly_price); const spbFmt = fmtMoney(travel_spb); const outsideFmt = fmtMoney(travel_outside); const techBlock = tech_list && tech_list.trim() ? `

Перечень техники, подлежащей бесплатной установке:

${escHtml(tech_list.trim())}

` : ""; return `
АКТ СДАЧИ-ПРИЁМКИ РАБОТ
по договору на сборку и установку мебели
№${escHtml(contract_num)} от ${escHtml(dp.day)} ${escHtml(dp.month)} ${escHtml(dp.year)} г.
г. Санкт-Петербург «${escHtml(dp.day)}» ${escHtml(dp.month)} ${escHtml(dp.year)} г.

Индивидуальный предприниматель, именуемый в дальнейшем «Исполнитель», с одной стороны и ${escHtml(client_name || "—")}, именуемый(ая) в дальнейшем «Заказчик» с другой стороны, составили настоящий акт сдачи-приёмки работ о нижеследующем:

Работы по установке мебели на объекте Заказчика по адресу: ${escHtml(address || "—")} по договору №${escHtml(contract_num)} от ${escHtml(dp.day)} ${escHtml(dp.month)} ${escHtml(dp.year)} г., на общую сумму ${escHtml(totalFmt)}, выполнены Исполнителем в полном объёме надлежащего качества.

Стоимость услуг по сборке и установке: ${escHtml(asmFmt)}
Стоимость выезда сборщика по СПб: ${escHtml(spbFmt)}
Стоимость выезда сборщика за пределы условной границы СПб: ${escHtml(outsideFmt)}

Стороны не имеют претензий друг к другу по исполнению Договора, в том числе по срокам выполнения работ, качеству и объёму работ.
Настоящий акт составлен в двух экземплярах.

${techBlock}

ВНИМАНИЕ! Перед подписанием акта тщательно осмотрите мебель на предмет возможных недостатков. После подписания акта приёмки претензии по качеству не принимаются.

При наличии вопросов обращайтесь в отдел сервиса: +7-952-379-63-25

ЗАКАЗЧИК _________________ / ${escHtml(client_name || "—")}
ИСПОЛНИТЕЛЬ _______________ / Васильев Р.Г.
`; } /* ── Главный экран ─────────────────────────────────────────── */ async function mount(container, assemblyId) { // Читаем id из параметра или из хэша const asmId = assemblyId || location.hash.split("/").pop(); container.innerHTML = ""; document.body.classList.remove("has-bottom-nav"); document.getElementById("bottom-nav")?.remove(); /* Заголовок */ const header = el(`
Акт сдачи-приёмки
`); header.querySelector(".podbor-back").addEventListener("click", () => { haptic && haptic("impact"); history.back(); }); const screen = el(`
`); container.appendChild(header); container.appendChild(screen); /* Loader */ screen.innerHTML = `
Загружаем данные…
`; /* Загружаем данные */ let data; try { data = await _api("contract_preview", { assembly_id: asmId }); } catch (e) { screen.innerHTML = `
Ошибка загрузки:
${escHtml(e.message)}
`; return; } if (!data.ok) { screen.innerHTML = `
${escHtml(data.error || "Не удалось загрузить данные")}
`; return; } const asm = data.assembly || {}; const contract = data.contract || {}; /* Начальные значения редактируемых полей */ let extras = { contract_num: contract.contract_num || String(asmId), contract_date: contract.contract_date || today(), travel_spb: contract.travel_spb != null ? contract.travel_spb : 0, travel_outside: contract.travel_outside != null ? contract.travel_outside : 0, tech_list: contract.tech_list || "", }; /* Вычисляем total_sum */ function calcTotal() { return (parseFloat(asm.assembly_price) || 0) + (parseFloat(extras.travel_spb) || 0) + (parseFloat(extras.travel_outside) || 0); } /* Рендерим всё */ function render() { screen.innerHTML = ""; /* === Блок: Акт === */ const actSection = el(`
`); actSection.innerHTML = buildActHtml({ contract_num: extras.contract_num, contract_date: extras.contract_date, client_name: asm.client_name || "", address: asm.address || "", total_sum: calcTotal(), assembly_price: asm.assembly_price || 0, travel_spb: extras.travel_spb, travel_outside: extras.travel_outside, tech_list: extras.tech_list, }); actSection.querySelector("#printActBtn")?.addEventListener("click", () => { window.print(); }); screen.appendChild(actSection); /* === Блок: Статус подписи === */ if (asm.signed_by_name) { const signedBadge = el(`
Акт подписан
${escHtml(asm.signed_by_name)} ${asm.signed_at ? " · " + escHtml(asm.signed_at) : ""}
`); screen.appendChild(signedBadge); } /* === Блок: Дополнительные данные === */ const extrasSection = el(`
✏️ Дополнительные данные
${!asm.signed_by_name ? ` ` : ""}
`); screen.appendChild(extrasSection); /* === Обработчики изменений — live-обновление акта === */ const liveInputs = [ ["inp_contract_num", "contract_num", false], ["inp_contract_date", "contract_date", false], ["inp_travel_spb", "travel_spb", true], ["inp_travel_outside", "travel_outside", true], ["inp_tech_list", "tech_list", false], ]; liveInputs.forEach(([id, key, isNum]) => { const inp = screen.querySelector("#" + id); if (!inp) return; inp.addEventListener("input", () => { extras[key] = isNum ? (parseFloat(inp.value) || 0) : inp.value; // Обновляем только акт, не весь экран (чтобы не потерять фокус ввода) const actDiv = actSection.querySelector("div"); if (actDiv) { const newInner = buildActHtml({ contract_num: extras.contract_num, contract_date: extras.contract_date, client_name: asm.client_name || "", address: asm.address || "", total_sum: calcTotal(), assembly_price: asm.assembly_price || 0, travel_spb: extras.travel_spb, travel_outside: extras.travel_outside, tech_list: extras.tech_list, }); const tmp = document.createElement("div"); tmp.innerHTML = newInner; const newActDiv = tmp.firstElementChild; actDiv.replaceWith(newActDiv); newActDiv.querySelector("#printActBtn")?.addEventListener("click", () => window.print()); } }); }); /* === Кнопка: Сохранить === */ screen.querySelector("#btnSave")?.addEventListener("click", async () => { haptic && haptic("impact"); const statusEl = screen.querySelector("#saveStatus"); statusEl.textContent = "Сохраняем…"; statusEl.style.color = "var(--muted)"; try { const res = await _api("contract_save", { assembly_id: asmId, contract_num: extras.contract_num, contract_date: extras.contract_date, travel_spb: extras.travel_spb, travel_outside: extras.travel_outside, tech_list: extras.tech_list, }); if (res.ok) { statusEl.textContent = "✅ Сохранено"; statusEl.style.color = "#27ae60"; setTimeout(() => { statusEl.textContent = ""; }, 3000); } else { throw new Error(res.error || "Ошибка сервера"); } } catch (e) { statusEl.textContent = "❌ " + e.message; statusEl.style.color = "#e74c3c"; } }); /* === Кнопка: Подписать акт === */ screen.querySelector("#btnSign")?.addEventListener("click", () => { haptic && haptic("impact"); if (typeof SignRequest !== "undefined") { SignRequest.open(asmId, { clientName: asm.client_name || "", clientTgId: asm.client_tg_id || null, onSuccess: () => { // Перезагружаем экран после успешной подписи mount(container, asmId); }, }); } else { alert("Модуль подписания недоступен"); } }); } // end render() render(); } // end mount() return { mount }; })();