// 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 (
toggle(g)} style={{
width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 12, padding: "16px 18px", background: "transparent", border: "none",
cursor: "pointer", textAlign: "left", color: "var(--ink-2)",
}}>
{g}
{!isMain && (
дополнительно
)}
{groupSensors.length} поз.
{selCount > 0 && (
{selCount} выбрано
)}
{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 */}
{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 === "sending" ? "Отправляем…" : <>Сформировать КП и счёт >}
{kpStatus === "done" ? (
Заявка отправлена. КП № {kpNumber} сформировано.
) : kpStatus === "error" ? (
Не удалось отправить заявку. Позвоните нам: +7 950 052-77-12
) : (
{canSubmit ? "Готовое предложение с оплатой по QR" : "Укажите имя и контакт, чтобы сформировать КП"}
)}
Сбросить к базовому пакету
{/* ---------------- КП / СЧЁТ (сворачиваемый) ---------------- */}
setKpOpen((o) => !o)} aria-expanded={kpOpen} aria-controls="kp-doc"
style={{
width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14,
padding: "16px 22px", border: "none", cursor: "pointer",
background: "var(--ink-2)", color: "#EEF3FA",
borderRadius: kpOpen ? "18px 18px 0 0" : 18,
textAlign: "left", fontFamily: "var(--font)",
}}>
Коммерческое предложение и счёт
№ {kpNumber} · {configName} · {bldMoney(total)}
{kpOpen ? "Свернуть" : "Развернуть"}
{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}
{/* Реквизиты ИП */}
{/* ============ СПЕЦИФИКАЦИЯ И СТОИМОСТЬ ============ */}
Спецификация и стоимость
{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]) => (
))}
)}
{/* ============ QR-ОПЛАТА (интеграция с Точкой / заглушка) ============ */}
{/* QR */}
{payLink && !qrError ? (
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)}
{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 ГК РФ является полным и безоговорочным
акцептом оферты и подтверждает заключение договора на изложенных ниже условиях.
Предмет договора — поставка оборудования, монтаж и настройка системы «умный дом» в составе и по стоимости, указанным в спецификации настоящего КП.
Заказ, оформленный без предварительной консультации со специалистом Исполнителя, принимается к исполнению только после 100% оплаты его стоимости.
Если оплата заказа произведена без предварительной консультации , Исполнитель вправе в одностороннем порядке отказаться от исполнения такого заказа, уведомив об этом Заказчика, и возвращает уплаченные денежные средства в полном объёме в течение 14 (четырнадцати) рабочих дней с даты принятия решения об отказе.
Сроки и порядок выполнения работ согласуются Сторонами после поступления оплаты. {T.warranty}.
Цены указаны в рублях РФ. {T.vat} (применяется упрощённая система налогообложения). {T.validity}.
{/* actions (скрываются при печати) */}
);
};
const Stepper = ({ n, onChange, dark }) => (
onChange(n - 1)} disabled={n <= 0} style={btnStep(dark, n <= 0)}>
{n}
onChange(n + 1)} style={btnStep(dark, false)}>
);
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;