// ============================================================================ // РазумДом — общие данные, расчёт сметы, конфигурация интеграций // ВСЕ настройки, которые нужно подключить «вживую», собраны в CONFIG ниже. // ============================================================================ const RD_CONFIG = { // ------------------------------------------------------------------------- // 1) ОТПРАВКА ЗАЯВОК В GOOGLE-ТАБЛИЦУ + ПИСЬМО НА ПОЧТУ // Разверните скрипт google-apps-script.gs (инструкция в файле // «НАСТРОЙКА.md»), получите URL вида // https://script.google.com/macros/s/XXXXX/exec // и вставьте его сюда вместо строки-заглушки. // Письма о заявках уходят на gdp888@ya.ru (задаётся в самом скрипте). // ------------------------------------------------------------------------- sheetEndpoint: "https://script.google.com/macros/s/AKfycbxr7YwWo5OtVNcLPzDd9oQ4Vt2fT5SVMAkKVpUHljFTCDoRTUu7GItSvQR0tiibbIl9yg/exec", // ------------------------------------------------------------------------- // 2) ОПЛАТА — ЭКВАЙРИНГ «ТОЧКА БАНК» // Вставьте вашу платёжную ссылку Точки (или статическую СБП-ссылку // вида https://qr.nspk.ru/...). Если appendSum=true, к ссылке // добавятся сумма и назначение платежа — QR-код строится автоматически. // ------------------------------------------------------------------------- payment: { baseUrl: "https://razumdom38.ru/pay", // ← заменить на ссылку Точка-банка appendSum: true, label: "Оплата · Точка Банк", }, // ------------------------------------------------------------------------- // 3) РЕКВИЗИТЫ ДЛЯ КП / СЧЁТА (отредактируйте под себя) // ------------------------------------------------------------------------- company: { brand: "РазумДом", legal: "ИП Петров Г.Д.", fullName: "Индивидуальный предприниматель Петров Геннадий Дмитриевич", inn: "381211966404", ogrnip: "321385000096431", rs: "40802810501500221486", // расчётный счёт ks: "30101810745374525104", // корреспондентский счёт legalAddress: "664047, г. Иркутск, ул. Советская, 126", taxMode: "УСН, без НДС", bankName: "ООО «Банк Точка», г. Москва", bik: "044525104", bankInn: "9721194461", bankKpp: "997950001", site: "razumdom38.ru", phone: "+7 950 052-77-12", email: "Baikal.Infolink@gmail.com", city: "Иркутск", }, // Гарантия и условия — печатаются в КП terms: { warranty: "Гарантия 24 месяца на оборудование и монтаж", validity: "Предложение действительно 14 дней", vat: "НДС не облагается", }, }; // --------------------------------------------------------------------------- // Каталог оборудования (цены «под ключ»: оборудование + монтаж + настройка) // «Заводской» каталог по умолчанию. Админ-панель хранит правки отдельно // (в localStorage) и накладывает их поверх — так можно править цены и // добавлять товары, не теряя возможности сбросить к заводским значениям. // --------------------------------------------------------------------------- const RD_DEFAULT_SENSORS = [ // ——— Основные блоки (раскрыты в конструкторе) ——— { id: "hub", group: "Сердце системы", name: "Контроллер Zigbee", desc: "Координатор сети устройств", price: 4800, icon: "wifi-off", required: true }, { id: "server", group: "Сердце системы", name: "Локальный сервер «Автоном»", desc: "Работа без интернета и облака", price: 18000, icon: "cloud-off" }, { id: "leak", group: "Защита от воды", name: "Датчик протечки", desc: "В углы санузла и под мойку", price: 1800, icon: "drop" }, { id: "valve", group: "Защита от воды", name: "Кран с электроприводом", desc: "Перекрывает воду по команде", price: 8900, icon: "valve" }, { id: "gas", group: "Защита от воды", name: "Датчик газа", desc: "Контроль концентрации на кухне", price: 3400, icon: "gauge" }, { id: "thermo", group: "Климат", name: "Датчик температуры", desc: "Комната или санузел", price: 1400, icon: "thermo" }, { id: "head", group: "Климат", name: "Термоголовка на радиатор", desc: "Управляет отоплением по сценарию", price: 6200, icon: "thermo" }, { id: "humid", group: "Климат", name: "Датчик влажности", desc: "Триггер на вентилятор", price: 1400, icon: "humid" }, { id: "humidifier", group: "Климат", name: "Управление увлажнителем", desc: "Автоматическое включение по влажности", price: 3900, icon: "humid" }, { id: "motion", group: "Безопасность", name: "Датчик движения", desc: "Свет и охрана по присутствию", price: 1600, icon: "motion" }, { id: "door", group: "Безопасность", name: "Датчик открытия", desc: "На двери и окна", price: 1200, icon: "door" }, { id: "cam", group: "Безопасность", name: "IP-камера", desc: "Локальная запись", price: 5800, icon: "camera" }, { id: "smoke", group: "Безопасность", name: "Датчик дыма", desc: "Оповещение при задымлении и сценарий безопасности", price: 2500, icon: "smoke" }, { id: "bulb", group: "Свет", name: "Умное реле / диммер", desc: "В люстру или линию света", price: 2400, icon: "bulb" }, { id: "smartbulb", group: "Свет", name: "Умная лампочка", desc: "Лампа с регулировкой яркости", price: 1500, icon: "bulb" }, { id: "button", group: "Свет", name: "Кнопка управления", desc: "Беспроводная кнопка для сценариев", price: 1500, icon: "switch" }, { id: "presence", group: "Свет", name: "Датчик присутствия", desc: "Точное определение людей в комнате", price: 3200, icon: "motion" }, // ——— Дополнительные устройства (свёрнуты в конструкторе) ——— { id: "roll", group: "Шторы и приводы", name: "Умные ролл-шторы", desc: "Рулонные шторы с электроприводом", price: 12900, icon: "blinds" }, { id: "curtain", group: "Шторы и приводы", name: "Умные раздвижные шторы", desc: "Электрокарниз для портьер", price: 16800, icon: "curtain" }, { id: "passctrl", group: "Свет и выключатели", name: "Проходной умный выключатель", desc: "Свет из 2–3 точек, сценарии", price: 2900, icon: "switch" }, { id: "rgb", group: "Свет и выключатели", name: "Угловой РГБ-светильник", desc: "Подсветка с миллионами оттенков", price: 4600, icon: "palette" }, { id: "weatherpanel", group: "Комфорт и техника", name: "Панель погоды", desc: "Погода на улице — в гардеробной или ванной", price: 8700, icon: "panel" }, { id: "mirror", group: "Комфорт и техника", name: "Умное зеркало", desc: "Подсветка, часы и погода в отражении", price: 21000, icon: "mirror" }, { id: "vacuum", group: "Комфорт и техника", name: "Умный пылесос-робот", desc: "Уборка по расписанию и сценарию", price: 24900, icon: "vacuum" }, { id: "kettle", group: "Комфорт и техника", name: "Умный чайник", desc: "Кипяток к пробуждению", price: 4200, icon: "kettle" }, { id: "ac25", group: "Климат-техника", name: "Умный кондиционер · до 25 м²", desc: "Сплит-система с управлением", price: 32000, icon: "snow" }, { id: "ac40", group: "Климат-техника", name: "Умный кондиционер · до 40 м²", desc: "Сплит-система с управлением", price: 39000, icon: "snow" }, { id: "ac50", group: "Климат-техника", name: "Умный кондиционер · до 50 м²", desc: "Сплит-система с управлением", price: 46000, icon: "snow" }, { id: "ac70", group: "Климат-техника", name: "Умный кондиционер · до 70 м²", desc: "Сплит-система с управлением", price: 58000, icon: "snow" }, { id: "ac100", group: "Климат-техника", name: "Умный кондиционер · до 100 м²", desc: "Сплит-система с управлением", price: 72000, icon: "snow" }, { id: "boiler", group: "Климат-техника", name: "Умный бойлер", desc: "Накопительный водонагреватель", price: 18500, icon: "boiler" }, { id: "heater", group: "Климат-техника", name: "Проточный водонагреватель", desc: "Горячая вода без бойлера", price: 14500, icon: "boiler" }, { id: "pool", group: "Бассейн и СПА", name: "Управление бассейном", desc: "Фильтрация, подогрев, подсветка", price: 48000, icon: "pool" }, { id: "sauna", group: "Бассейн и СПА", name: "Управление сауной", desc: "Прогрев к приходу, контроль t°", price: 36000, icon: "flame" }, ]; // Какие группы раскрыты в конструкторе изначально (основные). // Всё остальное (дополнительные устройства и новые группы) — свёрнуто. const RD_MAIN_GROUPS = ["Сердце системы", "Защита от воды", "Климат", "Безопасность", "Свет"]; // Предпочтительный порядок групп; новые группы добавляются в конец. const RD_GROUP_PREF = [ "Сердце системы", "Защита от воды", "Климат", "Безопасность", "Свет", "Шторы и приводы", "Свет и выключатели", "Комфорт и техника", "Климат-техника", "Бассейн и СПА", ]; // Иконки, доступные при добавлении товара в админ-панели const RD_ICON_OPTIONS = [ "bulb", "drop", "thermo", "shield", "valve", "motion", "door", "camera", "humid", "gauge", "wifi-off", "cloud-off", "fan", "blinds", "curtain", "switch", "mirror", "vacuum", "kettle", "snow", "boiler", "pool", "flame", "smoke", "panel", "palette", "house", "mic", "key", ]; // --- Персистентное хранилище правок каталога (localStorage) --------------- const RD_STORE_KEY = "rd_catalog_overrides_v1"; function rdLoadOverrides() { try { const raw = localStorage.getItem(RD_STORE_KEY); const o = raw ? JSON.parse(raw) : {}; return { edits: o.edits || {}, custom: o.custom || [], removed: o.removed || [] }; } catch (e) { return { edits: {}, custom: [], removed: [] }; } } function rdSaveOverrides() { try { localStorage.setItem(RD_STORE_KEY, JSON.stringify(RD_OVERRIDES)); } catch (e) {} } let RD_OVERRIDES = rdLoadOverrides(); // Сборка актуального каталога: заводской + правки + новые товары − удалённые function rdBuildCatalog() { const removed = new Set(RD_OVERRIDES.removed); const edits = RD_OVERRIDES.edits; const list = []; RD_DEFAULT_SENSORS.forEach((s) => { if (!removed.has(s.id)) list.push({ ...s }); }); RD_OVERRIDES.custom.forEach((c) => { if (!removed.has(c.id)) list.push({ ...c }); }); return list.map((s) => (edits[s.id] ? { ...s, ...edits[s.id] } : s)); } // Каталог, мутируемый «на месте», чтобы все замыкания видели обновления const RD_SENSORS = rdBuildCatalog(); // Подписчики на изменение каталога (для ре-рендера React) const RD_LISTENERS = new Set(); function rdNotify() { RD_LISTENERS.forEach((f) => { try { f(); } catch (e) {} }); } function rdOnCatalogChange(cb) { RD_LISTENERS.add(cb); return () => RD_LISTENERS.delete(cb); } function rdRefreshCatalog() { const fresh = rdBuildCatalog(); RD_SENSORS.length = 0; fresh.forEach((s) => RD_SENSORS.push(s)); rdNotify(); } // --- Админ-операции ------------------------------------------------------- function rdUpdateProduct(id, patch) { RD_OVERRIDES.edits[id] = { ...(RD_OVERRIDES.edits[id] || {}), ...patch }; rdSaveOverrides(); rdRefreshCatalog(); } function rdAddProduct(p) { const base = (p.name || "tovar").toLowerCase().replace(/[^a-zа-я0-9]+/gi, "").slice(0, 8) || "tovar"; let id = "x_" + base + "_" + Math.random().toString(36).slice(2, 6); RD_OVERRIDES.custom.push({ id, group: (p.group || "Дополнительно").trim(), name: (p.name || "Новый товар").trim(), desc: (p.desc || "").trim(), price: Math.max(0, Math.round(Number(p.price) || 0)), icon: p.icon || "bulb", }); rdSaveOverrides(); rdRefreshCatalog(); return id; } function rdDeleteProduct(id) { // удаляем из custom, если кастомный; иначе помечаем заводской как removed const wasCustom = RD_OVERRIDES.custom.some((c) => c.id === id); if (wasCustom) { RD_OVERRIDES.custom = RD_OVERRIDES.custom.filter((c) => c.id !== id); } else if (!RD_OVERRIDES.removed.includes(id)) { RD_OVERRIDES.removed.push(id); } delete RD_OVERRIDES.edits[id]; rdSaveOverrides(); rdRefreshCatalog(); } function rdResetCatalog() { RD_OVERRIDES = { edits: {}, custom: [], removed: [] }; rdSaveOverrides(); rdRefreshCatalog(); } // Текущий порядок групп: предпочтительные сперва, новые — в конец function rdGroupOrder() { const present = []; RD_SENSORS.forEach((s) => { if (!present.includes(s.group)) present.push(s.group); }); const ordered = RD_GROUP_PREF.filter((g) => present.includes(g)); present.forEach((g) => { if (!ordered.includes(g)) ordered.push(g); }); return ordered; } // --------------------------------------------------------------------------- // Готовые пакеты — ЧЁТКО ОПРЕДЕЛЁННЫЙ СОСТАВ. // compose: { sensorId: количество }. Цена пакета считается из состава. // --------------------------------------------------------------------------- const RD_PACKAGES = [ { id: "base", name: "Базовый", tagline: "Старт для новостройки", color: "var(--leak)", icon: "drop", blurb: "Защита от протечки и свет в прихожей. Минимум, с которого начинают все.", compose: { hub: 1, leak: 2, door: 1, bulb: 1 }, // = 12 000 ₽ }, { id: "intro", name: "Знакомство с Умным домом", tagline: "Первый шаг", color: "var(--good)", icon: "bulb", blurb: "Свет, климат и защита от протечки — попробовать умный дом без большого бюджета.", compose: { hub: 1, thermo: 1, presence: 1, leak: 2, motion: 1, button: 1, smartbulb: 2 }, }, { id: "standard", name: "Стандарт", tagline: "Популярный выбор", color: "var(--ink-2)", icon: "thermo", blurb: "Климат по комнатам, базовая безопасность и сценарии «утро / вечер / охрана».", compose: { hub: 1, leak: 3, valve: 1, thermo: 2, motion: 1, door: 2, smoke: 1, bulb: 1 }, }, { id: "auto", name: "Автоном", tagline: "Без облака", color: "var(--accent)", icon: "cloud-off", flagship: true, blurb: "Локальный сервер: вся система работает без интернета. Голос и сценарии — всегда.", compose: { server: 1, hub: 1, leak: 3, valve: 1, thermo: 2, motion: 1, door: 2, smoke: 1, bulb: 2, presence: 1 }, }, ]; // --------------------------------------------------------------------------- // Расчёт сметы из набора { id: qty } // --------------------------------------------------------------------------- function rdComputeQuote(picked) { const items = Object.entries(picked || {}) .map(([id, qty]) => { const s = RD_SENSORS.find((x) => x.id === id); if (!s || qty <= 0) return null; return { id, name: s.name, group: s.group, price: s.price, qty, sum: s.price * qty, icon: s.icon }; }) .filter(Boolean) .sort((a, b) => { const order = rdGroupOrder(); return order.indexOf(a.group) - order.indexOf(b.group); }); const pieces = items.reduce((a, b) => a + b.qty, 0); const total = items.reduce((a, b) => a + b.sum, 0); return { items, pieces, total }; } // Оценка экономии за 5 лет от состава умного дома (консервативная). // Привязана к выбранным устройствам — цифра «живая» для каждого КП. function rdEstimateSavings(items) { const qty = (id) => { const it = (items || []).find((x) => x.id === id); return it ? it.qty : 0; }; let annual = 0; // Климат — отопление и кондиционирование по сценарию annual += qty("head") * 4200; annual += qty("thermo") * 1400; annual += qty("humid") * 600; annual += (qty("ac25") + qty("ac40") + qty("ac50") + qty("ac70") + qty("ac100")) * 3200; annual += qty("boiler") * 2400 + qty("heater") * 1800; // Свет — автоматическое отключение annual += qty("bulb") * 900 + qty("presence") * 1500 + qty("motion") * 700; annual += qty("passctrl") * 600 + qty("rgb") * 300; // Защита от воды — амортизированный предотвращённый ущерб annual += qty("leak") * 1500 + qty("valve") * 3500; // Прочая техника — расписания и энергоэффективность annual += qty("vacuum") * 700 + qty("kettle") * 250 + qty("pool") * 9000 + qty("sauna") * 4000; // Шторы — инсоляция/теплопотери annual += qty("roll") * 800 + qty("curtain") * 900; const fiveYear = Math.round(annual * 5); return { annual: Math.round(annual), fiveYear }; } // Совпадает ли текущая выборка с составом готового пакета function rdMatchPackage(picked) { for (const p of RD_PACKAGES) { const a = Object.entries(p.compose).filter(([, q]) => q > 0); const b = Object.entries(picked || {}).filter(([, q]) => q > 0); if (a.length !== b.length) continue; const same = a.every(([id, q]) => (picked[id] || 0) === q); if (same) return p.name; } return null; } // Настроена ли реальная оплата Точки? Заглушку razumdom38.ru/pay не считаем. function rdPaymentConfigured() { const u = (RD_CONFIG.payment && RD_CONFIG.payment.baseUrl || "").trim(); if (!u) return false; if (/razumdom38\.ru\/pay\/?$/i.test(u)) return false; // дефолтная заглушка if (/PASTE_|example\.com|ВАША_ССЫЛКА/i.test(u)) return false; return true; } // Ссылка на оплату (эквайринг Точка) с суммой и назначением function rdBuildPayLink(total, kpNumber) { const p = RD_CONFIG.payment; if (!p.baseUrl) return ""; if (!p.appendSum) return p.baseUrl; const sep = p.baseUrl.includes("?") ? "&" : "?"; const purpose = `Оплата по КП ${kpNumber} (РазумДом)`; return `${p.baseUrl}${sep}sum=${total}&purpose=${encodeURIComponent(purpose)}`; } function rdQrSrc(data, size = 260) { return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&margin=0&qzone=1&data=${encodeURIComponent(data)}`; } function rdMoney(n) { return (n || 0).toLocaleString("ru-RU") + " ₽"; } // Полностью самодостаточный HTML коммерческого предложения для письма/вложения. // Email-safe: таблицы + инлайн-стили + hex-цвета (без CSS-переменных). function rdBuildKpHtml({ items, pieces, total, configName, kpNumber, payLink, payOk }) { const C = RD_CONFIG.company; const T = RD_CONFIG.terms; const sv = rdEstimateSavings(items || []); const today = new Date().toLocaleDateString("ru-RU", { day: "2-digit", month: "long", year: "numeric" }); const rub = (n) => Number(n || 0).toLocaleString("ru-RU"); const reqLine = [C.legal, C.inn && ("ИНН " + C.inn), C.ogrnip && ("ОГРНИП " + C.ogrnip)].filter(Boolean).join(" · "); const rows = (items || []).map((it) => `