diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js index 58588b9..fd5ec6f 100644 --- a/miniapp/assets/app.js +++ b/miniapp/assets/app.js @@ -909,6 +909,22 @@ async function renderStaff(me) { app.appendChild(assemblySection); renderStaffAssemblies(assemblySection.querySelector("#assemblyList")); } + + // Шпаргалки сборщика — прайс, рейки, полкодержатели + if (caps.assembler) { + const toolsBtn = el(` +
+ +
+ `); + toolsBtn.querySelector("button").addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = "#/master/tools"; + }); + app.appendChild(toolsBtn); + } } async function renderStaffAssemblies(container) { @@ -1679,6 +1695,14 @@ function routeByHash() { else init(); } else if (location.hash.startsWith("#/assembly")) { Assembly.mount(app); + } else if (location.hash.startsWith("#/master/tools")) { + if (typeof MasterTools !== "undefined") { + const h = location.hash; + if (h === "#/master/tools/rails") MasterTools.mountRails(app); + else if (h === "#/master/tools/shelves") MasterTools.mountShelves(app); + else if (h === "#/master/tools/price") MasterTools.mountPrice(app); + else MasterTools.mountMenu(app); + } else init(); } else if (location.hash.startsWith("#/master")) { const me = window.__zovMe; if (me) renderStaff(me); else init(); diff --git a/miniapp/assets/master_tools.js b/miniapp/assets/master_tools.js new file mode 100644 index 0000000..2d5fe8e --- /dev/null +++ b/miniapp/assets/master_tools.js @@ -0,0 +1,537 @@ +/* ============================================================ + MasterTools — шпаргалки сборщика + #/master/tools → меню инструментов + #/master/tools/rails → калькулятор реек + #/master/tools/shelves→ калькулятор полкодержателей + #/master/tools/price → прайс на доп. работы 2025 + ============================================================ */ + +const MasterTools = (function () { + "use strict"; + + /* ── Helpers ─────────────────────────────────────────────── */ + function el(html) { + const t = document.createElement("template"); + t.innerHTML = html.trim(); + return t.content.firstChild; + } + function escHtml(s) { + return String(s == null ? "" : s) + .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + function fmt(n) { return Math.round(n).toLocaleString("ru-RU"); } + + function _header(title, backHash) { + const h = el(` +
+ +
${escHtml(title)}
+
+
+ `); + h.querySelector(".podbor-back").addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = backHash || "#/master"; + }); + return h; + } + + function _screen(container) { + container.innerHTML = ""; + document.body.classList.remove("has-bottom-nav"); + document.getElementById("bottom-nav")?.remove(); + return container; + } + + function _numInput(id, label, value, placeholder, hint = "") { + return ` +
+ +
`; + } + + /* ================================================================ + МЕНЮ + ================================================================ */ + function mountMenu(container) { + _screen(container); + container.appendChild(_header("Шпаргалки сборщика", "#/master")); + + const wrap = el(`
`); + wrap.appendChild(el(` +
+

Расчётные таблицы и прайс — всё под рукой.

+
+ `)); + + const tools = [ + { hash: "#/master/tools/rails", icon: "📏", title: "Рейки на стену", + sub: "Расстояние от нулевой точки по количеству реек и ширине проёма" }, + { hash: "#/master/tools/shelves", icon: "📐", title: "Полкодержатели", + sub: "Расстояния внутри корпуса по высоте и количеству полок" }, + { hash: "#/master/tools/price", icon: "💰", title: "Прайс доп. работ 2025", + sub: "Все позиции с ценами — введите количество, получите итог" }, + ]; + + tools.forEach(t => { + const card = el(` +
+
${t.icon}
+
+
${escHtml(t.title)}
+
${escHtml(t.sub)}
+
+
+
+ `); + card.addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = t.hash; + }); + wrap.appendChild(card); + }); + + container.appendChild(wrap); + } + + /* ================================================================ + РЕЙКИ НА СТЕНУ + Алгоритм (из !1! РЕЙКИ.xlsx): + gap = (openingWidth − railCount × railWidth) / (railCount − 1) + position[0] = 0 + position[i] = round(i × (railWidth + gap)) + ================================================================ */ + function mountRails(container) { + _screen(container); + container.appendChild(_header("Рейки на стену", "#/master/tools")); + + let inputs = { width: 0, count: 8, railW: 51 }; + + const wrap = el(`
`); + + // Inputs + const formEl = el(` +
+
Параметры
+ ${_numInput("rt-width", "Длина проёма (мм)", "", "например: 2400")} + ${_numInput("rt-count", "Количество реек", 8, "например: 8")} + ${_numInput("rt-railw", "Ширина рейки (мм)", 51, "стандарт 51 мм", "Стандартная рейка ЗОВ — 51 мм")} + +
+ `); + wrap.appendChild(formEl); + + // Result + const resultEl = el(`
`); + wrap.appendChild(resultEl); + container.appendChild(wrap); + + // Events + ["rt-width","rt-count","rt-railw"].forEach(id => { + document.getElementById(id)?.addEventListener("input", () => _calcRails(inputs, resultEl)); + }); + document.getElementById("rt-calc")?.addEventListener("click", () => { + haptic && haptic("impact"); + _calcRails(inputs, resultEl); + }); + + function _calcRails(inp, out) { + const w = parseFloat(document.getElementById("rt-width")?.value) || 0; + const n = parseInt(document.getElementById("rt-count")?.value) || 0; + const rw = parseFloat(document.getElementById("rt-railw")?.value) || 51; + out.innerHTML = ""; + + if (!w || w <= 0) { out.innerHTML = `
Укажите длину проёма
`; return; } + if (n < 2) { out.innerHTML = `
Минимум 2 рейки
`; return; } + if (rw <= 0) { out.innerHTML = `
Укажите ширину рейки
`; return; } + if (n * rw >= w) { out.innerHTML = `
Рейки не помещаются в проём
`; return; } + + const gap = (w - n * rw) / (n - 1); + const positions = Array.from({ length: n }, (_, i) => + i === 0 ? 0 : Math.round(i * (rw + gap)) + ); + + let rows = positions.map((p, i) => ` +
+
Рейка ${i + 1}
+
+ ${p === 0 ? "0" : fmt(p)} мм +
+
+ `).join(""); + + out.innerHTML = ` +
+
Расстояния от нулевой точки
+ ${rows} +
+
+ Межреечное расстояние: ${fmt(gap)} мм · + Итого рейками: ${fmt(n * rw)} мм · + Итого зазорами: ${fmt(w - n * rw)} мм +
+ `; + } + } + + /* ================================================================ + ПОЛКОДЕРЖАТЕЛИ + Алгоритм (из !!! ПОЛКОДЕРЖАТЕЛИ.xlsx): + step = height / shelfCount + position[i] = (i+1) × step − drop + ================================================================ */ + function mountShelves(container) { + _screen(container); + container.appendChild(_header("Полкодержатели", "#/master/tools")); + + const wrap = el(`
`); + + const formEl = el(` +
+
Параметры
+ ${_numInput("sh-height", "Высота проёма корпуса (мм)", "", "например: 915")} + ${_numInput("sh-count", "Количество полок", 3, "например: 3")} + ${_numInput("sh-drop", "Опуск на полкодержатель (мм)", 5, "стандарт 5 мм", + "Стандартное значение — 5 мм")} + +
+ `); + wrap.appendChild(formEl); + + const resultEl = el(`
`); + wrap.appendChild(resultEl); + container.appendChild(wrap); + + document.getElementById("sh-calc")?.addEventListener("click", () => { + haptic && haptic("impact"); + _calcShelves(resultEl); + }); + ["sh-height","sh-count","sh-drop"].forEach(id => { + document.getElementById(id)?.addEventListener("input", () => _calcShelves(resultEl)); + }); + + function _calcShelves(out) { + const h = parseFloat(document.getElementById("sh-height")?.value) || 0; + const n = parseInt(document.getElementById("sh-count")?.value) || 0; + const drop = parseFloat(document.getElementById("sh-drop")?.value) ?? 5; + out.innerHTML = ""; + + if (!h || h <= 0) { out.innerHTML = `
Укажите высоту проёма
`; return; } + if (n < 1) { out.innerHTML = `
Минимум 1 полка
`; return; } + + const step = h / n; + const positions = Array.from({ length: n }, (_, i) => + Math.round((i + 1) * step - drop) + ); + + const rows = positions.map((p, i) => ` +
+
Полка ${i + 1}
+
+ ${fmt(p)} мм +
+
+ `).join(""); + + out.innerHTML = ` +
+
Расстояния от нижней кромки корпуса
+ ${rows} +
+
+ Шаг между полками: ${fmt(step)} мм +
+ `; + } + } + + /* ================================================================ + ПРАЙС НА ДОП. РАБОТЫ 2025 + Формула: стоимость = цена × количество (если кол-во введено) + Итог = сумма всех позиций + ================================================================ */ + + const PRICE_SECTIONS = [ + { id: "general", title: "Общие работы", items: [ + { id:"g1", name:"Выезд мастера в магазин по просьбе клиента (>5км +40р/км)", price:800 }, + { id:"g2", name:"Доп. срочный выезд мастера по просьбе клиента в течение 24 часов", price:3000 }, + { id:"g3", name:"Технологический выпил (один выпил в одной детали)", price:100 }, + { id:"g4", name:"Ложный выезд или ожидание заказчика более 45 мин", price:1500 }, + { id:"g5", name:"Вынос картонной упаковки в коридор", price:0, free:true }, + { id:"g6", name:"Вынос упаковки в мусорный контейнер (лифт + до 100м)", price:0, free:true }, + ]}, + { id: "light", title: "Подсветка", items: [ + { id:"l1", name:"Подключение светодиодной ленты по наружной части мебели, за пм", price:600 }, + { id:"l2", name:"Подключение светодиодной ленты внутри мебели, за 1 линию", price:400 }, + { id:"l3", name:"Монтаж декоративных планок на стену, руб./метр", price:370 }, + { id:"l4", name:"Фрезеровка канала для врезной подсветки, руб./метр", price:1000 }, + ]}, + { id: "sink", title: "Зона мойки", items: [ + { id:"s1", name:"Выпил в столешнице / стенке ЛДСП под трубы", price:300 }, + { id:"s2", name:"Демонтаж обесточенной розетки", price:50 }, + { id:"s3", name:"Переделка модуля по месту (от), за шт.", price:1500 }, + { id:"s4", name:"Изготовление отверстия в керамограните (с предоставлением сверла)", price:300 }, + { id:"s5", name:"Демонтаж старой мойки", price:500 }, + { id:"s6", name:"Врезка накладной мойки Покупателя с обработкой выпила (без подкл.)", price:800 }, + { id:"s7", name:"Установка мойки Покупателя (без подключения)", price:500 }, + { id:"s8", name:"Вырез отверстия под смеситель (металл)", price:300 }, + { id:"s9", name:"Вырез отверстия под смеситель (искусственный камень)", price:500 }, + { id:"s10", name:"Установка встраиваемой посудомоечной машины Покупателя (без подкл.)", price:2000 }, + { id:"s11", name:"Установка встраиваемой стиральной машины Покупателя (без подкл.)", price:2000 }, + { id:"s12", name:"Установка НЕ встраиваемой стиральной машины Покупателя (без подкл.)", price:400 }, + { id:"s13", name:"Изготовление отверстия в столешнице компакт-плита (варочная/мойка/розетка/диспенсер)", price:2000 }, + { id:"s14", name:"Установка мойки подстольного монтажа", price:3000 }, + ]}, + { id: "fridge", title: "Установка холодильника", items: [ + { id:"f1", name:"Установка холодильника Покупателя без перенавески дверей (без подкл.)", price:2500 }, + { id:"f2", name:"Перенавеска дверей холодильника без электроники", price:500 }, + { id:"f3", name:"Перенавеска дверей холодильника с электроникой", price:800 }, + { id:"f4", name:"Демонтаж обесточенной розетки", price:50 }, + ]}, + { id: "hob", title: "Варочная поверхность", items: [ + { id:"h1", name:"Технологический выпил", price:300 }, + { id:"h2", name:"Переделка модуля по месту (от)", price:1000 }, + { id:"h3", name:"Врезка варочной поверхности Покупателя с обработкой выпила (без подкл.)", price:800 }, + { id:"h4", name:"Установка варочной панели Покупателя (без подключения)", price:500 }, + { id:"h5", name:"Вырез в столешнице под шахту / выступ", price:500 }, + ]}, + { id: "hood", title: "Зона вытяжки", items: [ + { id:"ho1", name:"Технологический выпил", price:100 }, + { id:"ho2", name:"Вырез под розетку", price:200 }, + { id:"ho3", name:"Установка купольной вытяжки 60 см (без подключения)", price:1500 }, + { id:"ho4", name:"Установка купольной вытяжки 90 см (без подключения)", price:2000 }, + { id:"ho5", name:"Установка плоской вытяжки (без подключения)", price:300 }, + { id:"ho6", name:"Установка встраиваемой вытяжки (без подключения)", price:1500 }, + { id:"ho7", name:"Установка полновстроенной вытяжки (без подключения)", price:1500 }, + { id:"ho8", name:"Установка островной вытяжки (без подключения)", price:3000 }, + { id:"ho9", name:"Подключение гофрированного воздуховода к вытяжке, шт.", price:400 }, + { id:"ho10",name:"Подключение пластикового воздуховода к вытяжке с монтажом, руб./метр", price:800 }, + { id:"ho11",name:"Установка фланца", price:300 }, + ]}, + { id: "oven", title: "Духовой шкаф / микроволновка", items: [ + { id:"o1", name:"Вырез под розетку", price:200 }, + { id:"o2", name:"Переделка модуля по месту (от)", price:700 }, + { id:"o3", name:"Установка духового шкафа Покупателя в модуль (без подключения)", price:600 }, + { id:"o4", name:"Установка встраиваемой микроволновой печи Покупателя (без подкл.)", price:1000 }, + ]}, + { id: "wall", title: "Зона стеновой панели", items: [ + { id:"w1", name:"Вырез под розетку в стеновой панели", price:200 }, + { id:"w2", name:"Вырез под розетку в стеновой панели компакт-плита, шт.", price:500 }, + { id:"w3", name:"Перепил одного декоративного элемента за деталь", price:400 }, + { id:"w4", name:"Переделка модуля по месту (от)", price:700 }, + { id:"w5", name:"Врезка круглых светильников Покупателя (без подключения), шт.", price:70 }, + { id:"w6", name:"Установка релинга Покупателя (1 пм или 1 шт)", price:300 }, + { id:"w7", name:"Продольный пил цоколя по вине Покупателя за 1 пм", price:200 }, + { id:"w8", name:"Изготовление подиума под плиту (материалами заказчика)", price:400 }, + { id:"w9", name:"Демонтаж/монтаж пристеночного плинтуса, руб./метр", price:200 }, + { id:"w10", name:"Присадка ручек Покупателя за 1 отверстие", price:40 }, + { id:"w11", name:"Установка ручек Покупателя за 1 шт", price:40 }, + { id:"w12", name:"Планировка розеток на место установки кухни", price:2500 }, + ]}, + { id: "cabinet", title: "Шкаф", items: [ + { id:"c1", name:"Монтаж доборов для установки натяжного потолка, р/мп", price:2000 }, + { id:"c2", name:"Демонтаж обесточенной розетки", price:50 }, + { id:"c3", name:"Вырез под розетку / коммуникации", price:200 }, + { id:"c4", name:"Перепил одного декоративного элемента за деталь", price:400 }, + { id:"c5", name:"Переделка модуля по месту (от)", price:2000 }, + { id:"c6", name:"Врезка круглых светильников Покупателя (без подключения), шт.", price:70 }, + { id:"c7", name:"Продольный пил цоколя по вине Покупателя за 1 пм", price:200 }, + { id:"c8", name:"Присадка ручек Покупателя за 1 отверстие", price:40 }, + { id:"c9", name:"Установка ручек Покупателя за 1 шт", price:40 }, + ]}, + { id: "extra", title: "Доп. работы мастер Васильев Р.Г.", items: [ + { id:"e1", name:"Установка и подключение розетки", price:200 }, + { id:"e2", name:"Подключение варочной поверхности", price:800 }, + { id:"e3", name:"Подключение посудомоечной машины", price:1600 }, + { id:"e4", name:"Подключение стиральной машины", price:1600 }, + { id:"e5", name:"Установка сушильной машины", price:1000 }, + { id:"e6", name:"Подключение смесителя", price:1600 }, + { id:"e7", name:"Подключение слива мойки", price:1000 }, + { id:"e8", name:"Установка и подключение измельчителя", price:2500 }, + { id:"e9", name:"Установка и подключение фильтра для воды", price:1000 }, + { id:"e10", name:"Установка дозатора", price:500 }, + { id:"e11", name:"Установка фасадной петли", price:200 }, + { id:"e12", name:"Вырез под вентрешетку", price:400 }, + { id:"e13", name:"Вырез в ЛДСП с кромлением", price:1000 }, + { id:"e14", name:"Переделка сантехнической подводки / водоподготовка", price:1000 }, + { id:"e15", name:"Установка столешницы с заходом в подоконник, от р.", price:3000 }, + { id:"e16", name:"Монтаж и вырезы в панели в зоне инсталляции (примыкания силиконом)", price:3500 }, + { id:"e17", name:"Вырез под коммуникации (трубы), от р.", price:200 }, + { id:"e18", name:"Распаковка / установка / подключение отдельностоящего холодильника", price:500 }, + { id:"e19", name:"Монтаж вешалки (Заказчика)", price:200 }, + { id:"e20", name:"Монтаж брючницы на направляющей (Заказчика)", price:700 }, + { id:"e21", name:"Присадка и монтаж полки скрытого монтажа", price:700 }, + ]}, + { id: "transport", title: "Транспортные расходы", special: "transport", items: [ + { id:"t1", name:"Выезд за КАД, руб./км × дней (40 р/км)", price:40, special:"transport" }, + ]}, + ]; + + // Состояние калькулятора (qty по каждому item id) + let _priceQty = {}; + let _transKm = 0; + let _transDays = 1; + + function mountPrice(container) { + _screen(container); + _priceQty = {}; + _transKm = 0; _transDays = 1; + container.appendChild(_header("Прайс доп. работ 2025", "#/master/tools")); + + const wrap = el(`
`); + + wrap.appendChild(el(` +
+

+ Введите количество в нужных строках — итог пересчитается автоматически. +

+
+ `)); + + PRICE_SECTIONS.forEach(section => { + const secEl = el(` +
+
${escHtml(section.title)}
+
+ `); + + if (section.special === "transport") { + // Особый блок: км × дней + const transEl = el(` +
+
+ Выезд за КАД — 40 руб/км, за каждый день +
+
+ + +
0 ₽
+
+
+ `); + secEl.appendChild(transEl); + wrap.appendChild(secEl); + + document.getElementById("pr-km")?.addEventListener("input", e => { + _transKm = parseFloat(e.target.value) || 0; + _updateTransSum(); _updateTotal(); + }); + document.getElementById("pr-days")?.addEventListener("input", e => { + _transDays = parseFloat(e.target.value) || 1; + _updateTransSum(); _updateTotal(); + }); + return; + } + + wrap.appendChild(secEl); + + section.items.forEach(item => { + const rowEl = el(` +
+
+
+ ${escHtml(item.name)} +
+
+ ${item.free ? "Бесплатно" : fmt(item.price) + " ₽"} +
+
+ ${item.free ? `
` : ` + +
+ `} +
+ `); + + if (!item.free) { + const inp = rowEl.querySelector(`#qty-${item.id}`); + inp?.addEventListener("input", e => { + const q = parseFloat(e.target.value) || 0; + _priceQty[item.id] = q; + const sumEl = document.getElementById(`sum-${item.id}`); + if (sumEl) sumEl.textContent = q > 0 ? fmt(item.price * q) + " ₽" : ""; + _updateTotal(); + }); + } + wrap.appendChild(rowEl); + }); + }); + + // Итого (sticky внизу) + const totalBar = el(` +
+
Итого работ
+
0 ₽
+
+ `); + container.appendChild(wrap); + container.appendChild(totalBar); + + function _updateTotal() { + let total = 0; + PRICE_SECTIONS.forEach(sec => { + if (sec.special === "transport") { + total += _transKm * 40 * (_transDays || 1); + return; + } + sec.items.forEach(item => { + if (!item.free) total += (item.price * (_priceQty[item.id] || 0)); + }); + }); + const el = document.getElementById("pr-total"); + if (el) el.textContent = fmt(total) + " ₽"; + } + + function _updateTransSum() { + const el = document.getElementById("pr-trans-sum"); + const sum = _transKm * 40 * (_transDays || 1); + if (el) el.textContent = sum > 0 ? fmt(sum) + " ₽" : "0 ₽"; + } + } + + return { mountMenu, mountRails, mountShelves, mountPrice }; +})(); diff --git a/miniapp/index.html b/miniapp/index.html index 1a2973a..89d6f89 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -51,6 +51,7 @@ +