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(`
+
+ `);
+ 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 руб/км, за каждый день
+
+
+
+ `);
+ 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(`
+
+ `);
+ 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 @@
+