// Package builder — controlled by shared state (lifted to App). // Starts on the "Базовый" preset (~12 000 ₽). Renders a live quote AND a // formal КП/Счёт document with a Tochka payment QR. const { SENSORS: BLD_SENSORS, MAIN_GROUPS: BLD_MAIN, computeQuote: bldQuote, matchPackage: bldMatch, buildPayLink: bldPayLink, qrSrc: bldQr, estimateSavings: bldSavings, paymentConfigured: bldPayOk, money: bldMoney, CONFIG: BLD_CFG, scrollTo: bldScroll } = window.RD; const Builder = ({ picked, setPicked, presetName, setPresetName, kpNumber, setConsult }) => { const set = (id, n) => { setPicked((p) => { const next = { ...p }; if (n <= 0) delete next[id]; else next[id] = Math.min(n, 30); return next; }); setPresetName(null); // ручное изменение → индивидуальная конфигурация }; // Сброс к базовому пакету const resetToBase = () => { const base = window.RD.PACKAGES[0]; setPicked({ ...base.compose }); setPresetName(base.name); setConsult && setConsult(false); }; // Контактные поля для отправки КП const [leadName, setLeadName] = React.useState(""); const [leadContact, setLeadContact] = React.useState(""); const [kpStatus, setKpStatus] = React.useState("idle"); // idle | sending | done | error const [kpOpen, setKpOpen] = React.useState(false); // КП свёрнут по умолчанию const canSubmit = leadName.trim().length >= 2 && leadContact.trim().length >= 3; // Сворачиваемые блоки: основные раскрыты, дополнительные — свёрнуты const groups = window.RD.groupOrder(); const [openGroups, setOpenGroups] = React.useState({}); const isOpen = (g) => (g in openGroups ? openGroups[g] : BLD_MAIN.includes(g)); const toggle = (g) => setOpenGroups((o) => ({ ...o, [g]: !isOpen(g) })); const { items, pieces, total } = bldQuote(picked); const matched = presetName || bldMatch(picked); const configName = matched || "Индивидуальная конфигурация"; // Сформировать КП: скролл к документу + отправка заявки const formKP = async () => { if (!canSubmit || kpStatus === "sending") return; setKpStatus("sending"); setKpOpen(true); bldScroll("kp-section"); const payload = window.RD.buildLeadPayload({ name: leadName, contact: leadContact, objectType: "", picked, configName, kpNumber, interest: "Сформировал КП в калькуляторе", }); const res = await window.RD.submitLead(payload); setKpStatus(res.ok ? "done" : "error"); }; return (
Конструктор

Соберите пакет под свою квартиру или частный дом.

Меняйте состав — смета, коммерческое предложение и счёт с QR-оплатой пересчитываются мгновенно. Финальную цену согласуем после осмотра объекта.

{/* Catalog */}
{groups.map((g) => { const groupSensors = BLD_SENSORS.filter((s) => s.group === g); if (groupSensors.length === 0) return null; const open = isOpen(g); const isMain = BLD_MAIN.includes(g); const selCount = groupSensors.reduce((a, s) => a + (picked[s.id] || 0), 0); return (
{open && (
{groupSensors.map((s) => { const n = picked[s.id] || 0; const active = n > 0; return (
{s.name} {s.required && · нужен}
{s.desc}
{bldMoney(s.price)}
set(s.id, v)} dark={active} />
); })}
)}
); })}
{/* Live summary */}
Ваш проект
{configName}
{total.toLocaleString("ru-RU")}
{pieces} устройств · монтаж и настройка включены

{items.length === 0 && (
Ничего не выбрано. Начните с хаба и пары датчиков.
)} {items.map((it) => (
{it.name} × {it.qty} {bldMoney(it.sum)}
))}
setLeadName(e.target.value)} placeholder="Ваше имя" autoComplete="name" aria-label="Ваше имя" style={bldInput} /> setLeadContact(e.target.value)} placeholder="Телефон или Telegram" inputMode="tel" autoComplete="tel" aria-label="Телефон или Telegram" style={bldInput} />
{kpStatus === "done" ? (
Заявка отправлена. КП № {kpNumber} сформировано.
) : kpStatus === "error" ? (
Не удалось отправить заявку. Позвоните нам: +7 950 052-77-12
) : (
{canSubmit ? "Готовое предложение с оплатой по QR" : "Укажите имя и контакт, чтобы сформировать КП"}
)}
{/* ---------------- КП / СЧЁТ (сворачиваемый) ---------------- */}
{kpOpen && }
); }; // ----- Коммерческое предложение + Счёт + QR-оплата (одностраничный документ) ----- const Proposal = ({ items, pieces, total, configName, kpNumber }) => { const [qrError, setQrError] = React.useState(false); const payLink = bldPayLink(total, kpNumber); const payOk = bldPayOk(); const today = new Date().toLocaleDateString("ru-RU", { day: "2-digit", month: "long", year: "numeric" }); const C = BLD_CFG.company; const T = BLD_CFG.terms; const sv = bldSavings(items); if (items.length === 0) { return (
Выберите оборудование или готовый пакет — здесь появится коммерческое предложение со счётом и QR-оплатой.
); } // Реквизиты ИП — собираем только заполненные поля const reqLine = [C.legal, C.inn && ("ИНН " + C.inn), C.ogrnip && ("ОГРНИП " + C.ogrnip)].filter(Boolean).join(" · "); const contactLine = [C.phone, C.email, C.site].filter(Boolean).join(" · "); return (
{/* ============ ШАПКА: логотип + реквизиты ИП ============ */}
{C.brand}
Умный дом под ключ · {C.city}
Коммерческое предложение
№ {kpNumber}
от {today}
{/* Реквизиты ИП */}
{reqLine}
{contactLine}
{/* ============ СПЕЦИФИКАЦИЯ И СТОИМОСТЬ ============ */}

Спецификация и стоимость

{configName}
{/* table head */}
Позиция Кол. Цена Сумма
{items.map((it) => (
{it.name} {it.qty} {it.price.toLocaleString("ru-RU")} {it.sum.toLocaleString("ru-RU")}
))} {/* totals */}
Оборудование, монтаж и настройка {bldMoney(total)}
Проектирование сценариев и обучение в подарок
Итого к оплате {total.toLocaleString("ru-RU")}
{pieces} устройств · {T.vat} · {T.validity.toLowerCase()}
{/* ============ РЕКЛАМНЫЙ БЛОК: ЭКОНОМИЯ ЗА 5 ЛЕТ ============ */} {sv.fiveYear > 0 && (
Окупаемость
За 5 лет этот умный дом вернёт вам около
{sv.fiveYear.toLocaleString("ru-RU")} ₽
≈ {sv.annual.toLocaleString("ru-RU")} ₽ в год на отоплении, свете и предотвращённых протечках. Оценка консервативная — обычно выше.
{[["thermo", "Климат по сценарию"], ["bulb", "Свет только когда нужен"], ["drop", "Защита от протечек"]].map(([ic, t]) => (
{t}
))}
)} {/* ============ QR-ОПЛАТА (интеграция с Точкой / заглушка) ============ */}
{/* QR */}
{payLink && !qrError ? ( QR для оплаты через Точка Банк setQrError(true)} style={{ width: 172, height: 172, borderRadius: 12, background: "#fff", padding: 9, border: "1px solid var(--line)", filter: payOk ? "none" : "grayscale(0.25)" }} /> ) : (
Здесь будет QR-код оплаты Точка Банк
)} {!payOk && ( образец )}
{/* Оплата */}
{BLD_CFG.payment.label}
{payOk ? "Наведите камеру телефона на QR-код" : "Оплата по QR подключается через эквайринг Точка"}
Сумма по счёту {bldMoney(total)}
{payOk && ( Оплатить онлайн )}
{T.warranty} {T.vat}
{/* ============ РЕКВИЗИТЫ ИП ДЛЯ ОПЛАТЫ ============ */}
Реквизиты для оплаты
{C.fullName}
{[ ["ИНН", C.inn], ["ОГРНИП", C.ogrnip], ["Расчётный счёт", C.rs], ["Корр. счёт", C.ks], ["Юр. адрес", C.legalAddress], ["Налогообложение", C.taxMode], ["Банк", C.bankName], ["БИК", C.bik], ["ИНН / КПП банка", C.bankInn + " / " + C.bankKpp], ["Назначение платежа", "Оплата по КП № " + kpNumber], ].map(([k, v]) => (
{k} {v}
))}
{/* ============ ПУБЛИЧНАЯ ОФЕРТА (СЧЁТ-ДОГОВОР) ============ */}
Публичная оферта · счёт-договор
в соответствии с законодательством РФ

Настоящий документ является публичной офертой (счётом-договором) {C.fullName}, {C.taxMode}. Оплата настоящего счёта в соответствии со ст. 435 и п. 3 ст. 438 ГК РФ является полным и безоговорочным акцептом оферты и подтверждает заключение договора на изложенных ниже условиях.

  1. Предмет договора — поставка оборудования, монтаж и настройка системы «умный дом» в составе и по стоимости, указанным в спецификации настоящего КП.
  2. Заказ, оформленный без предварительной консультации со специалистом Исполнителя, принимается к исполнению только после 100% оплаты его стоимости.
  3. Если оплата заказа произведена без предварительной консультации, Исполнитель вправе в одностороннем порядке отказаться от исполнения такого заказа, уведомив об этом Заказчика, и возвращает уплаченные денежные средства в полном объёме в течение 14 (четырнадцати) рабочих дней с даты принятия решения об отказе.
  4. Сроки и порядок выполнения работ согласуются Сторонами после поступления оплаты. {T.warranty}.
  5. Цены указаны в рублях РФ. {T.vat} (применяется упрощённая система налогообложения). {T.validity}.
{/* actions (скрываются при печати) */}
{ e.preventDefault(); bldScroll("contact"); }} className="btn btn-ghost"> Отправить заявку с этим КП Состав и сумма автоматически прикрепятся к заявке.
); }; const Stepper = ({ n, onChange, dark }) => (
{n}
); const btnStep = (dark, dis) => ({ width: 26, height: 26, borderRadius: 999, border: "none", background: "transparent", color: dark ? "#EEF3FA" : "var(--ink-2)", opacity: dis ? 0.3 : 1, cursor: dis ? "default" : "pointer", display: "grid", placeItems: "center", }); const bldInput = { width: "100%", height: 46, background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: 12, padding: "0 14px", color: "var(--ink-2)", fontSize: 15, fontFamily: "var(--font)", outline: "none", }; window.Builder = Builder;