PageSourceSearch

https://br.divnomorskoe.ru/widget-booking.js

js divnomorskoe.ru collected 2026-10-04 22:56:42 UTC 104,217 bytes, 2,484 lines download raw bytes

1/**
2 * Встраиваемый виджет поиска номеров (публичный API /api/v1/widget/*).
3 * Использование:
4 *   Один блок: BookingWidget.init({ mount: '#id', apiBaseUrl, organizationId, token, legalBaseUrl });
5 *   Форма и результаты в разных местах страницы: те же поля + formMount и resultsMount
6 *   (оба контейнера должны быть в DOM до вызова init; скрипт подключается один раз).
7 *   Горящие предложения: BookingWidget.init({ mount, mode: 'hot-offers', layout: 'horizontal'|'vertical'|'tiles', ... });
8 */
9(function (global) {
10  "use strict";
11
12  function qs(sel, root) {
13    return (root || document).querySelector(sel);
14  }
15
16  function el(tag, attrs, children) {
17    var n = document.createElement(tag);
18    if (attrs) {
19      Object.keys(attrs).forEach(function (k) {
20        if (k === "class") n.className = attrs[k];
21        else if (k === "html") n.innerHTML = attrs[k];
22        else if (k.slice(0, 2) === "on" && typeof attrs[k] === "function") n.addEventListener(k.slice(2), attrs[k]);
23        else n.setAttribute(k, attrs[k]);
24      });
25    }
26    (children || []).forEach(function (c) {
27      if (c) n.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
28    });
29    return n;
30  }
31
32  function legalHref(path, extra) {
33    var b = String((extra && extra.legalBaseUrl) || "https://bronefix.ru").replace(/\/$/, "");
34    return b + path;
35  }
36
37  function appendPdConsentAndSubmit(modal, opts) {
38    var termsId = opts.termsId || "bw-guest-terms";
39    var consentId = opts.consentId;
40    var termsCb = el("input", { type: "checkbox", id: termsId });
41    var consentCb = el("input", { type: "checkbox", id: consentId });
42    var submitBtn = el(
43      "button",
44      {
45        type: "button",
46        id: opts.submitId,
47        class: "primary",
48        disabled: "disabled",
49        style: opts.style || "margin-top:8px",
50        onclick: opts.onSubmit,
51      },
52      ["Отправить заявку"],
53    );
54    function sync() {
55      submitBtn.disabled = !(termsCb.checked && consentCb.checked);
56    }
57    termsCb.addEventListener("change", sync);
58    consentCb.addEventListener("change", sync);
59    var termsA = el("a", { href: legalHref("/legal/guest-terms", opts), target: "_blank", rel: "noopener" }, [
60      "пользовательским соглашением модуля",
61    ]);
62    var pdA = el("a", { href: legalHref("/legal/guest-consent-template", opts), target: "_blank", rel: "noopener" }, [
63      "согласием на обработку персональных данных",
64    ]);
65    modal.appendChild(el("label", { class: "bw-consent" }, [termsCb, " Принимаю ", termsA]));
66    modal.appendChild(el("label", { class: "bw-consent" }, [consentCb, " Даю ", pdA]));
67    modal.appendChild(submitBtn);
68    return { consentCb: consentCb, termsCb: termsCb, submitBtn: submitBtn };
69  }
70
71  function syncSubmitWithPdConsent(submitEl, consentEl, termsEl) {
72    if (!submitEl) return;
73    submitEl.disabled = !(consentEl && consentEl.checked && termsEl && termsEl.checked);
74  }
75
76  function init(opts) {
77    if (String((opts && opts.mode) || "").toLowerCase() === "hot-offers") {
78      initHotOffers(opts || {});
79      return;
80    }
81    var apiBase = (opts.apiBaseUrl || "").replace(/\/$/, "");
82    var orgId = opts.organizationId || "";
83    var token = opts.token || "";
84    if (opts.metrikaCounterId) {
85      var mc = String(opts.metrikaCounterId).replace(/\D/g, "");
86      if (mc.length >= 5) {
87        window.__bronefixMetrikaId = window.__bronefixMetrikaId || mc;
88      }
89    }
90
91    function resolveMount(m) {
92      return typeof m === "string" ? qs(m) : m;
93    }
94
95    var formRoot = null;
96    var resultsRoot = null;
97    var useSplitShell = false;
98
99    if (opts.formMount && opts.resultsMount) {
100      formRoot = resolveMount(opts.formMount);
101      resultsRoot = resolveMount(opts.resultsMount);
102      if (!formRoot || !resultsRoot) {
103        var hint = formRoot || resultsRoot;
104        if (hint) {
105          hint.innerHTML = "";
106          hint.appendChild(
107            el("p", { class: "booking-widget-error" }, ["Не найден formMount или resultsMount"]),
108          );
109        }
110        return;
111      }
112      useSplitShell = formRoot !== resultsRoot;
113    } else {
114      var mount = opts.mount;
115      var single = resolveMount(mount);
116      if (!single) return;
117      formRoot = single;
118      resultsRoot = single;
119    }
120
121    if (!apiBase || !orgId || !token) {
122      formRoot.appendChild(el("p", { class: "booking-widget-error" }, ["Нужны apiBaseUrl, organizationId и token"]));
123      return;
124    }
125
126    var landing = (function () {
127      var p = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
128      return {
129        offer: p.get("offer") || "",
130        room: p.get("room") || "",
131        in: p.get("in") || "",
132        out: p.get("out") || "",
133        adults: p.get("adults") || "",
134        rate: p.get("rate") || "",
135        utm_source: p.get("utm_source") || "",
136        utm_medium: p.get("utm_medium") || "",
137        utm_campaign: p.get("utm_campaign") || "",
138        utm_content: p.get("utm_content") || "",
139        utm_term: p.get("utm_term") || "",
140        yclid: p.get("yclid") || "",
141      };
142    })();
143
144    function pushMetrikaGoal(name) {
145      try {
146        if (typeof window.ym === "function" && window.__bronefixMetrikaId) {
147          window.ym(window.__bronefixMetrikaId, "reachGoal", name);
148        }
149      } catch (e) {}
150    }
151
152    function pushEcommerce(action, product) {
153      try {
154        window.dataLayer = window.dataLayer || [];
155        var payload = { ecommerce: { currencyCode: "RUB" } };
156        payload.ecommerce[action] = { products: [product] };
157        window.dataLayer.push(payload);
158      } catch (e) {}
159    }
160
161    function trackBookingPaid(offerId, name, priceRub) {
162      pushMetrikaGoal("booking_paid");
163      if (offerId) {
164        pushEcommerce("purchase", {
165          id: offerId,
166          name: name || "booking",
167          price: priceRub || 0,
168          quantity: 1,
169        });
170      }
171    }
172    window.__bronefixTrackBookingPaid = trackBookingPaid;
173
174    var state = {
175      config: null,
176      categories: [],
177      transitionOptions: [],
178      nearbyDateWindows: [],
179      selected: null,
180      selectedStay: null,
181      lastSearch: null,
182      captchaToken: null,
183      nearbyLoading: false,
184      contactPhone: "",
185      maxUrl: "",
186      landing: landing,
187      resolvedOffer: null,
188    };
189
190    formRoot.innerHTML = "";
191    formRoot.className = (formRoot.className ? formRoot.className + " " : "") + "booking-widget-root";
192    if (useSplitShell) {
193      resultsRoot.innerHTML = "";
194      resultsRoot.className = (resultsRoot.className ? resultsRoot.className + " " : "") + "booking-widget-root";
195    }
196
197    var style = el("style", {}, []);
198    style.textContent =
199      ".booking-widget-root{font-family:system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.45;color:#0f172a;width:100%;max-width:none;box-sizing:border-box}" +
200      ".booking-widget-root .bw-panel{border:1px solid #e2e8f0;border-radius:12px;padding:12px 12px 10px;background:#fff}" +
201      ".booking-widget-root .bw-panel+.bw-panel{margin-top:12px}" +
202      ".booking-widget-root .bw-results-panel.bw-results-panel--hidden{display:none}" +
203      ".booking-widget-root .bw-panel-title{margin:0 0 10px;font-size:15px;font-weight:650;color:#0f172a}" +
204      ".booking-widget-root .bw-panel-note{margin:0 0 10px}" +
205      ".booking-widget-root input,.booking-widget-root select,.booking-widget-root textarea,.booking-widget-root button{border-radius:8px;border:1px solid #cbd5e1;padding:8px 10px}" +
206      ".booking-widget-root button.primary{background:#0284c7;color:#fff;border-color:#0284c7;cursor:pointer}" +
207      ".booking-widget-root button.primary:disabled{opacity:.55;cursor:not-allowed}" +
208      ".booking-widget-root .bw-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;align-items:stretch}" +
209      ".booking-widget-root .bw-cta{flex:1 1 120px;background:#0284c7;color:#fff;border:1px solid #0284c7;border-radius:8px;padding:8px 12px;cursor:pointer;font-weight:600;font-size:14px;text-align:center;text-decoration:none;display:inline-flex;align-items:center;justify-content:center}" +
210      ".booking-widget-root .bw-cta:hover{filter:brightness(1.05)}" +
211      ".booking-widget-root .bw-cta--secondary{background:#fff;color:#0f172a;border-color:#cbd5e1;font-weight:600}" +
212      ".booking-widget-root .bw-cta--secondary:hover{background:#f8fafc;filter:none}" +
213      ".booking-widget-root .bw-cta--max{background:#1a1a2e;color:#fff;border-color:#1a1a2e}" +
214      ".booking-widget-root .bw-cta--max:hover{filter:brightness(1.15)}" +
215      ".booking-widget-root .bw-consent{display:flex;align-items:flex-start;gap:8px;margin:12px 0 4px;font-size:13px;color:#334155;cursor:pointer;line-height:1.35}" +
216      ".booking-widget-root .bw-consent input{margin:2px 0 0;flex-shrink:0;width:16px;height:16px;accent-color:#0284c7}" +
217      ".booking-widget-root .row{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin-bottom:10px}" +
218      ".booking-widget-root .field{display:flex;flex-direction:column;gap:4px}" +
219      ".booking-widget-root .bw-field-err{color:#b91c1c;font-size:12px;line-height:1.25}" +
220      ".booking-widget-root .bw-invalid{border-color:#ef4444 !important;box-shadow:0 0 0 2px rgba(239,68,68,.12)}" +
221      ".booking-widget-root .bw-captcha-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}" +
222      ".booking-widget-root .bw-captcha-q{font-size:13px;color:#334155}" +
223      ".booking-widget-root .muted{color:#64748b;font-size:12px}" +
224      ".booking-widget-root .bw-cards{display:flex;flex-direction:column;gap:12px;margin:12px 0}" +
225      ".booking-widget-root .bw-card{border:1px solid #e2e8f0;border-radius:12px;padding:12px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.06)}" +
226      ".booking-widget-root .bw-card--sel{border-color:#0284c7;background:#f0f9ff;box-shadow:0 2px 8px rgba(2,132,199,.12)}" +
227      ".booking-widget-root .bw-card-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start}" +
228      ".booking-widget-root .bw-gal{flex:0 1 420px;min-width:min(100%,220px);max-width:100%}" +
229      ".booking-widget-root .bw-gal-main{position:relative;border-radius:10px;overflow:hidden;background:#f1f5f9;aspect-ratio:4/3;max-height:280px}" +
230      ".booking-widget-root .bw-gal-main img,.booking-widget-root .bw-gal-main video{width:100%;height:100%;object-fit:cover;display:block;cursor:pointer}" +
231      ".booking-widget-root .bw-gal-main .bw-vplay{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.35);cursor:pointer}" +
232      ".booking-widget-root .bw-gal-main .bw-vplay span{width:52px;height:52px;border-radius:50%;background:rgba(255,255,255,.95);display:flex;align-items:center;justify-content:center;font-size:22px;padding-left:4px}" +
233      ".booking-widget-root .bw-thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}" +
234      ".booking-widget-root .bw-thumb{width:56px;height:56px;border-radius:8px;overflow:hidden;border:2px solid transparent;cursor:pointer;background:#e2e8f0;flex-shrink:0}" +
235      ".booking-widget-root .bw-thumb--on{border-color:#0284c7}" +
236      ".booking-widget-root .bw-thumb img{width:100%;height:100%;object-fit:cover;display:block}" +
237      ".booking-widget-root .bw-thumb--vid{position:relative;display:flex;align-items:center;justify-content:center;font-size:20px;background:#1e293b;color:#fff}" +
238      ".booking-widget-root .bw-card-body{flex:1 1 280px;min-width:min(100%,220px)}" +
239      ".booking-widget-root .bw-title{font-size:16px;font-weight:650;margin:0 0 6px}" +
240      ".booking-widget-root .bw-price{font-size:15px;font-weight:600;color:#0f172a;margin:4px 0}" +
241      ".booking-widget-root .bw-price-sub{font-size:12px;font-weight:400;color:#64748b}" +
242      ".booking-widget-root .bw-tr-badge{display:inline-block;font-size:11px;font-weight:600;color:#0369a1;background:#e0f2fe;border-radius:999px;padding:2px 8px;margin-bottom:6px}" +
243      ".booking-widget-root .bw-tr-seg{font-size:13px;color:#334155;margin:4px 0;padding:6px 8px;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0}" +
244      ".booking-widget-root .bw-tr-seg strong{color:#0f172a;font-weight:600}" +
245      ".booking-widget-root .bw-facts{display:flex;flex-wrap:wrap;gap:8px 14px;margin:8px 0;font-size:12px;color:#475569}" +
246      ".booking-widget-root .bw-amenity-groups{margin:4px 0 2px;min-width:0}" +
247      ".booking-widget-root .bw-amenity-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:8px;row-gap:2px;align-items:start}" +
248      "@media (min-width:900px){.booking-widget-root .bw-amenity-list{grid-template-columns:repeat(3,minmax(0,1fr))}}" +
249      "@media (max-width:360px){.booking-widget-root .bw-amenity-list{grid-template-columns:1fr}}" +
250      ".booking-widget-root .bw-amenity-item{display:block;margin:0;padding:0;min-width:0}" +
251      ".booking-widget-root .bw-amenity-row{display:flex;align-items:flex-start;gap:4px;min-width:0;font-size:10px;line-height:1.25;color:#64748b}" +
252      ".booking-widget-root .bw-amenity-row>.amenity-icon{color:#0284c7;font-size:1.05em;line-height:inherit;flex-shrink:0;margin-top:.08em}" +
253      ".booking-widget-root .bw-amenity-cap{flex:1;min-width:0;word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-clamp:2}" +
254      ".booking-widget-root .bw-tags{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0}" +
255      ".booking-widget-root .bw-tag{font-size:11px;background:#f1f5f9;color:#334155;padding:3px 8px;border-radius:999px}" +
256      ".booking-widget-root .bw-desc{font-size:13px;color:#475569;margin:6px 0 10px;max-height:4.2em;overflow:hidden}" +
257      ".booking-widget-root .bw-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}" +
258      ".booking-widget-root .bw-search-msg{margin:10px 0;padding:10px 12px;border-radius:10px;border:1px solid #e2e8f0;background:#f8fafc;color:#334155;font-size:13px;line-height:1.35}" +
259      ".booking-widget-root .bw-search-msg--error{border-color:#fecaca;background:#fef2f2;color:#991b1b}" +
260      ".booking-widget-root .bw-nearby-actions{margin:8px 0 10px;display:flex;flex-direction:column;gap:8px}" +
261      ".booking-widget-root .bw-nearby-hint{margin:0;font-size:13px;color:#334155;line-height:1.35}" +
262      ".booking-widget-root .bw-nearby-chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}" +
263      ".booking-widget-root .bw-nearby-chip{background:#fff;color:#0369a1;border:1px solid #7dd3fc;cursor:pointer;font-size:13px;padding:7px 10px;border-radius:999px}" +
264      ".booking-widget-root .bw-nearby-chip:hover{background:#f0f9ff;border-color:#38bdf8}" +
265      ".booking-widget-root .bw-nearby-chip:disabled{opacity:.6;cursor:not-allowed}" +
266      ".booking-widget-root .bw-nearby-chip--active{background:#e0f2fe;border-color:#0284c7;color:#075985;font-weight:600}" +
267      ".booking-widget-root .bw-nearby-meta{font-size:12px;color:#64748b}" +
268      ".booking-widget-root .bw-nearby-window{margin-top:16px;padding:14px 14px 10px;border:1px solid #bae6fd;border-radius:12px;background:linear-gradient(180deg,#f0f9ff 0%,#fff 48%)}" +
269      ".booking-widget-root .bw-nearby-title{margin:0 0 6px;font-size:18px;font-weight:700;line-height:1.3;color:#0369a1}" +
270      ".booking-widget-root .bw-nearby-sub{margin:0 0 10px;font-size:13px;color:#0369a1}" +
271      ".booking-widget-root .bw-req-overlay{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:99998;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}" +
272      ".booking-widget-root .bw-req-modal{width:min(680px,96vw);max-height:88vh;overflow:auto;background:#fff;border-radius:12px;padding:14px;box-shadow:0 14px 40px rgba(2,6,23,.35);position:relative}" +
273      ".booking-widget-root .bw-req-close{position:absolute;top:8px;right:8px;width:32px;height:32px;border-radius:999px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;font-size:20px;line-height:1}" +
274      ".bw-lightbox{position:fixed;inset:0;background:rgba(15,23,42,.72);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}" +
275      ".bw-lightbox-inner{position:relative;max-width:min(960px,100%);max-height:90vh;background:#0f172a;border-radius:12px;overflow:hidden}" +
276      ".bw-lightbox-inner img,.bw-lightbox-inner video{max-width:100%;max-height:min(80vh,720px);display:block;margin:0 auto}" +
277      ".bw-lightbox-inner iframe{width:min(960px,96vw);height:min(540px,56.25vw);border:0;display:block;background:#000}" +
278      ".bw-lb-close{position:absolute;top:8px;right:8px;z-index:2;width:36px;height:36px;border-radius:999px;border:0;background:rgba(255,255,255,.92);cursor:pointer;font-size:20px;line-height:1}" +
279      ".bw-lb-nav{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;border-radius:999px;border:0;background:rgba(255,255,255,.92);cursor:pointer;font-size:22px;z-index:2}" +
280      ".bw-lb-prev{left:8px}.bw-lb-next{right:8px}";
281    formRoot.appendChild(style);
282    if (useSplitShell) {
283      var styleResults = el("style", {}, []);
284      styleResults.textContent = style.textContent;
285      resultsRoot.appendChild(styleResults);
286    }
287
288    function widgetPublicBase() {
289      try {
290        var scripts = document.getElementsByTagName("script");
291        for (var i = 0; i < scripts.length; i++) {
292          var src = scripts[i].src || "";
293          var idx = src.indexOf("widget-booking.js");
294          if (idx !== -1) return src.slice(0, idx).replace(/\/$/, "");
295        }
296      } catch (e) {}
297      return "";
298    }
299
300    function ensureAmenityIcons() {
301      try {
302        if (document.getElementById("bronefix-amenity-icons")) return;
303        var base = widgetPublicBase();
304        if (!base) return;
305        var link = document.createElement("link");
306        link.id = "bronefix-amenity-icons";
307        link.rel = "stylesheet";
308        link.href = base + "/fonts/amenities/amenities.css";
309        document.head.appendChild(link);
310      } catch (e) {}
311    }
312
313    function amenityIconClass(slug) {
314      var s = String(slug || "wifi");
315      if (!/^[a-z0-9_]+$/.test(s)) s = "wifi";
316      return "amenity-icon amenity-icon--" + s;
317    }
318
319    var errBox = el("div", { class: "muted" });
320    formRoot.appendChild(errBox);
321
322    function setErr(t) {
323      errBox.textContent = t || "";
324    }
325
326    function searchErrorRu(msg) {
327      var s = String(msg || "").trim();
328      if (!s) return "Ошибка поиска. Попробуйте изменить даты или состав гостей.";
329      var m = s.match(/^Minimum stay is (\d+) night\(s\)\.?$/i);
330      if (m) return "Минимальный срок проживания — " + m[1] + " ночи.";
331      if (/check_in must be before check_out/i.test(s)) return "Дата выезда должна быть позже даты заезда.";
332      if (/check_in must be today or later/i.test(s)) return "Дата заезда не может быть раньше сегодняшнего дня.";
333      if (/Too many requests/i.test(s)) return "Слишком много запросов. Попробуйте чуть позже.";
334      if (/No room available in this category/i.test(s)) return "В выбранной категории сейчас нет свободных номеров.";
335      if (/Room no longer available/i.test(s)) return "Выбранный номер уже заняли. Обновите поиск.";
336      return s;
337    }
338
339    function setResultsPanelVisible(visible) {
340      if (visible) resultsSection.classList.remove("bw-results-panel--hidden");
341      else resultsSection.classList.add("bw-results-panel--hidden");
342    }
343
344    function showSearchMessage(text, isError) {
345      setResultsPanelVisible(true);
346      listHost.innerHTML = "";
347      nearbyHost.innerHTML = "";
348      state.nearbyDateWindows = [];
349      renderNearbyAction();
350      listHost.appendChild(
351        el(
352          "div",
353          { class: "bw-search-msg" + (isError ? " bw-search-msg--error" : "") },
354          [text],
355        ),
356      );
357    }
358
359    function renderOfferNearbyAlternatives(windows) {
360      if (!windows || !windows.length) return;
361      var wrap = el("div", { class: "bw-nearby-window" }, []);
362      wrap.appendChild(
363        el("p", { class: "muted bw-nearby-sub" }, [
364          "Ближайшие свободные даты для этой категории:",
365        ]),
366      );
367      var row = el("div", { class: "bw-actions" }, []);
368      windows.slice(0, 8).forEach(function (win) {
369        var btn = el("button", { type: "button", class: "bw-cta bw-cta--secondary" }, [
370          formatIsoRu(win.check_in) + " – " + formatIsoRu(win.check_out),
371        ]);
372        btn.onclick = function () {
373          var L = state.landing;
374          if (L) {
375            L.in = win.check_in;
376            L.out = win.check_out;
377          }
378          applyLandingToForm();
379          runSearch();
380        };
381        row.appendChild(btn);
382      });
383      wrap.appendChild(row);
384      nearbyHost.appendChild(wrap);
385    }
386
387    function localTodayIso() {
388      var d = new Date();
389      var y = d.getFullYear();
390      var m = String(d.getMonth() + 1).padStart(2, "0");
391      var dd = String(d.getDate()).padStart(2, "0");
392      return y + "-" + m + "-" + dd;
393    }
394
395    var formSearch = el("div", {}, []);
396    formSearch.appendChild(el("div", { class: "row" }, [
397      el("div", { class: "field" }, [
398        el("label", {}, ["Заезд"]),
399        (function () {
400          var i = el("input", { type: "date" });
401          i.id = "bw-ci";
402          i.min = localTodayIso();
403          return i;
404        })(),
405      ]),
406      el("div", { class: "field" }, [
407        el("label", {}, ["Выезд"]),
408        (function () {
409          var i = el("input", { type: "date" });
410          i.id = "bw-co";
411          i.min = localTodayIso();
412          return i;
413        })(),
414      ]),
415      el("div", { class: "field" }, [
416        el("label", {}, ["Взрослых"]),
417        (function () {
418          var s = el("select", { id: "bw-ad" });
419          for (var a = 1; a <= 10; a++) s.appendChild(el("option", { value: String(a) }, [String(a)]));
420          return s;
421        })(),
422      ]),
423      el("div", { class: "field" }, [
424        el("label", {}, ["Детей"]),
425        (function () {
426          var s = el("select", { id: "bw-ch" });
427          for (var c = 0; c <= 10; c++) s.appendChild(el("option", { value: String(c) }, [String(c)]));
428          return s;
429        })(),
430      ]),
431      el(
432        "button",
433        {
434          type: "button",
435          class: "primary",
436          onclick: function () {
437            void runSearch();
438          },
439        },
440        ["Найти"],
441      ),
442    ]));
443    var searchSection = el("section", { class: "bw-panel bw-search-panel", "aria-label": "Поиск номера" }, []);
444    searchSection.appendChild(el("h2", { class: "bw-panel-title" }, ["Поиск номера"]));
445    searchSection.appendChild(formSearch);
446    formRoot.appendChild(searchSection);
447
448    function addDaysIso(iso, days) {
449      try {
450        var d = new Date(iso + "T00:00:00");
451        d.setDate(d.getDate() + days);
452        var y = d.getFullYear();
453        var m = String(d.getMonth() + 1).padStart(2, "0");
454        var dd = String(d.getDate()).padStart(2, "0");
455        return y + "-" + m + "-" + dd;
456      } catch (e) {
457        return iso;
458      }
459    }
460
461    function syncDateInputs(changed) {
462      var ciEl = qs("#bw-ci", formRoot);
463      var coEl = qs("#bw-co", formRoot);
464      if (!ciEl || !coEl) return;
465      var today = localTodayIso();
466      ciEl.min = today;
467      var ci = ciEl.value || "";
468      if (ci && ci < today) {
469        ciEl.value = today;
470        ci = today;
471      }
472      var co = coEl.value || "";
473      if (ci) coEl.min = ci;
474      else coEl.min = today;
475      if (!ci || !co) return;
476      if (co <= ci) {
477        if (changed === "co") {
478          coEl.value = addDaysIso(ci, 1);
479        } else {
480          coEl.value = addDaysIso(ci, 1);
481        }
482      }
483    }
484
485    (function attachDateSync() {
486      var ciEl = qs("#bw-ci", formRoot);
487      var coEl = qs("#bw-co", formRoot);
488      if (!ciEl || !coEl) return;
489      ["change", "input"].forEach(function (ev) {
490        ciEl.addEventListener(ev, function () {
491          syncDateInputs("ci");
492        });
493        coEl.addEventListener(ev, function () {
494          syncDateInputs("co");
495        });
496      });
497    })();
498
499    var resultsSection = el("section", { class: "bw-panel bw-results-panel", "aria-label": "Результаты поиска" }, []);
500    resultsSection.appendChild(el("h2", { class: "bw-panel-title" }, ["Результаты поиска"]));
501    var resultsNote = el("p", { class: "muted bw-panel-note" }, ["Выберите вариант, чтобы оставить заявку."]);
502    resultsSection.appendChild(resultsNote);
503    var nearbyActionHost = el("div", { class: "bw-nearby-actions" }, []);
504    resultsSection.appendChild(nearbyActionHost);
505    var listHost = el("div", {}, []);
506    resultsSection.appendChild(listHost);
507    var nearbyHost = el("div", {}, []);
508    resultsSection.appendChild(nearbyHost);
509    resultsSection.classList.add("bw-results-panel--hidden");
510    resultsRoot.appendChild(resultsSection);
511
512    var reqHost = el("div", { id: "bw-request-anchor" }, []);
513    resultsRoot.appendChild(reqHost);
514
515    function apiUrl(path, q) {
516      var p = path.charAt(0) === "/" ? path : "/" + path;
517      var u = new URL(apiBase + p);
518      Object.keys(q || {}
518).forEach(function (k) {
519        if (q[k] != null && q[k] !== "") u.searchParams.set(k, String(q[k]));
520      });
521      return u.toString();
522    }
523
524    /** Публичные файлы виджета требуют organization_id и token в query; полный http(s)-URL без них даёт 422. */
525    function widgetAssetUrl(path) {
526      if (!path) return "";
527      if (/^https?:\/\//i.test(path)) {
528        try {
529          var u = new URL(path);
530          if (u.pathname.indexOf("/api/v1/widget/") === 0) {
531            u.searchParams.set("organization_id", orgId);
532            u.searchParams.set("token", token);
533          }
534          return u.toString();
535        } catch (e) {
536          return path;
537        }
538      }
539      return apiUrl(path, { organization_id: orgId, token: token });
540    }
541
542    function galleryItemsForRoom(r) {
543      if (r.gallery_items && r.gallery_items.length) return r.gallery_items;
544      var g = [];
545      (r.media_urls || []).forEach(function (u) {
546        g.push({ kind: "image", url: u });
547      });
548      (r.video_urls || []).forEach(function (u) {
549        g.push({ kind: "video", url: u });
550      });
551      return g;
552    }
553
554    function youtubeEmbedUrl(url) {
555      try {
556        var u = new URL(url);
557        if (u.hostname.indexOf("youtu.be") >= 0) {
558          return "https://www.youtube.com/embed/" + u.pathname.replace(/^\//, "").split("?")[0];
559        }
560        if (u.hostname.indexOf("youtube.com") >= 0) {
561          var v = u.searchParams.get("v");
562          if (v) return "https://www.youtube.com/embed/" + v;
563          var em = u.pathname.match(/^\/embed\/([^/?]+)/);
564          if (em) return "https://www.youtube.com/embed/" + em[1];
565          var sh = u.pathname.match(/^\/shorts\/([^/?]+)/);
566          if (sh) return "https://www.youtube.com/embed/" + sh[1];
567        }
568      } catch (e) {}
569      return null;
570    }
571
572    function vimeoEmbedUrl(url) {
573      try {
574        var u = new URL(url);
575        if (u.hostname.indexOf("vimeo.com") < 0) return null;
576        var m = u.pathname.match(/\/(\d+)/);
577        if (m) return "https://player.vimeo.com/video/" + m[1];
578      } catch (e) {}
579      return null;
580    }
581
582    function rutubeEmbedUrl(url) {
583      try {
584        var u = new URL(url);
585        if (u.hostname.indexOf("rutube.ru") < 0) return null;
586        var em = u.pathname.match(/^\/play\/embed\/([a-z0-9_-]+)\/?$/i);
587        if (em) return "https://rutube.ru/play/embed/" + em[1] + "/";
588        var vid = u.pathname.match(/^\/video\/([a-z0-9_-]+)\/?$/i);
589        if (vid) return "https://rutube.ru/play/embed/" + vid[1] + "/";
590      } catch (e) {}
591      return null;
592    }
593
594    function vkEmbedUrl(url) {
595      try {
596        var u = new URL(url);
597        var host = u.hostname.toLowerCase();
598        if (host.indexOf("vk.com") < 0 && host.indexOf("vkvideo.ru") < 0) return null;
599        var m = u.pathname.match(/video(-?\d+)_(\d+)/i);
600        if (!m) return null;
601        return (
602          "https://vk.com/video_ext.php?oid=" +
603          encodeURIComponent(m[1]) +
604          "&id=" +
605          encodeURIComponent(m[2]) +
606          "&hd=2"
607        );
608      } catch (e) {}
609      return null;
610    }
611
612    function okEmbedUrl(url) {
613      try {
614        var u = new URL(url);
615        if (u.hostname.indexOf("ok.ru") < 0) return null;
616        var m = u.pathname.match(/\/video\/(\d+)/i);
617        if (m) return "https://ok.ru/videoembed/" + m[1];
618      } catch (e) {}
619      return null;
620    }
621
622    function embedUrlWithAutoplay(embedUrl) {
623      if (!embedUrl) return embedUrl;
624      return embedUrl + (embedUrl.indexOf("?") >= 0 ? "&" : "?") + "autoplay=1";
625    }
626
627    function isDirectVideoUrl(url) {
628      return /\.(mp4|webm|ogg|mov)(\?|$)/i.test(url || "");
629    }
630
631    function videoPlayableUrl(url) {
632      var resolved = widgetAssetUrl(url);
633      return resolved || url;
634    }
635
636    var lightboxEl = null;
637    var requestModalEl = null;
638    function clearFieldValidation() {
639      var bad = resultsRoot.querySelectorAll(".bw-invalid");
640      for (var i = 0; i < bad.length; i++) bad[i].classList.remove("bw-invalid");
641      var em = resultsRoot.querySelectorAll(".bw-field-err");
642      for (var j = 0; j < em.length; j++) em[j].remove();
643    }
644    function markRequiredField(inputEl) {
645      if (!inputEl) return;
646      var holder = inputEl.closest(".field") || inputEl.parentElement;
647      inputEl.classList.add("bw-invalid");
648      if (holder && !qs(".bw-field-err", holder)) {
649        holder.appendChild(el("div", { class: "bw-field-err" }, ["Обязательное поле"]));
650      }
651    }
652    function closeLightbox() {
653      if (lightboxEl && lightboxEl.parentNode) lightboxEl.parentNode.removeChild(lightboxEl);
654      lightboxEl = null;
655      document.removeEventListener("keydown", onLbKey);
656    }
657    function closeRequestModal() {
658      if (requestModalEl && requestModalEl.parentNode) requestModalEl.parentNode.removeChild(requestModalEl);
659      requestModalEl = null;
660    }
661    function onLbKey(ev) {
662      if (ev.key === "Escape") closeLightbox();
663    }
664
665    function openLightbox(room, startIdx) {
666      var items = galleryItemsForRoom(room);
667      if (!items.length) return;
668      var idx = Math.max(0, Math.min(startIdx || 0, items.length - 1));
669      closeLightbox();
670      var box = el("div", { class: "bw-lightbox", onclick: function (e) {
671        if (e.target === box) closeLightbox();
672      }});
673      var inner = el("div", { class: "bw-lightbox-inner" });
674      var body = el("div", {}, []);
675      function renderSlide() {
676        body.innerHTML = "";
677        var it = items[idx];
678        if (!it) return;
679        if (it.kind === "image") {
680          var iu = widgetAssetUrl(it.url);
681          body.appendChild(el("img", { src: iu, alt: "" }));
682        } else {
683          var u = it.url;
684          var y = youtubeEmbedUrl(u);
685          var vm = vimeoEmbedUrl(u);
686          var rt = rutubeEmbedUrl(u);
687          var vk = vkEmbedUrl(u);
688          var ok = okEmbedUrl(u);
689          var iframeAttrs = {
690            allowfullscreen: "true",
691            title: "video",
692            allow: "autoplay; fullscreen; encrypted-media; picture-in-picture",
693          };
694          if (y)
695            body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(y) }, iframeAttrs)));
696          else if (vm)
697            body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(vm) }, iframeAttrs)));
698          else if (rt)
699            body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(rt) }, iframeAttrs)));
700          else if (vk)
701            body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(vk) }, iframeAttrs)));
702          else if (ok)
703            body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(ok) }, iframeAttrs)));
704          else if (isDirectVideoUrl(u) || isDirectVideoUrl(videoPlayableUrl(u)))
705            body.appendChild(
706              el("video", {
707                src: videoPlayableUrl(u),
708                controls: "true",
709                autoplay: "true",
710                playsinline: "true",
711              }),
712            );
713          else
714            body.appendChild(
715              el("div", { style: "padding:24px;color:#fff;text-align:center" }, [
716                el("p", {}, ["Не удалось встроить видео."]),
717                el("a", { href: u, target: "_blank", rel: "noopener", style: "color:#7dd3fc" }, ["Открыть ссылку"]),
718              ]),
719            );
720        }
721      }
722      renderSlide();
723      inner.appendChild(el("button", { type: "button", class: "bw-lb-close", onclick: closeLightbox }, ["×"]));
724      if (items.length > 1) {
725        inner.appendChild(
726          el("button", {
727            type: "button",
728            class: "bw-lb-nav bw-lb-prev",
729            onclick: function (e) {
730              e.stopPropagation();
731              idx = (idx - 1 + items.length) % items.length;
732              renderSlide();
733            },
734          }, ["‹"]),
735        );
736        inner.appendChild(
737          el("button", {
738            type: "button",
739            class: "bw-lb-nav bw-lb-next",
740            onclick: function (e) {
741              e.stopPropagation();
742              idx = (idx + 1) % items.length;
743              renderSlide();
744            },
745          }, ["›"]),
746        );
747      }
748      inner.appendChild(body);
749      box.appendChild(inner);
750      document.body.appendChild(box);
751      lightboxEl = box;
752      document.addEventListener("keydown", onLbKey);
753    }
754
755    function fmtMoney(cents, cur) {
756      if (cents == null || cents === "") return null;
757      var n = Number(cents);
758      if (!isFinite(n)) return null;
759      return (n / 100).toFixed(0) + " " + (cur || "RUB");
760    }
761
762    function clearCardSel() {
763      var cards = resultsRoot.querySelectorAll(".bw-card");
764      for (var i = 0; i < cards.length; i++) {
765        cards[i].className = (cards[i].className || "").replace(/\bbw-card--sel\b/g, "").trim();
766      }
767    }
768
769    function openRequestFor(r, card, stayInfo) {
770      state.selected = r;
771      state.selectedStay = stayInfo || null;
772      clearCardSel();
773      if (card) card.className = ((card.className || "") + " bw-card--sel").trim();
774      renderForm();
775      pushMetrikaGoal("booking_start");
776      setTimeout(function () {
777        var gs = qs("#bw-gs", resultsRoot);
778        if (gs) gs.focus();
779      }, 300);
780    }
781
782    function renderAmenitiesBlock(r) {
783      ensureAmenityIcons();
784      var groups = r.amenity_groups;
785      if (groups && groups.length) {
786        var ul = el("ul", { class: "bw-amenity-list" });
787        for (var i = 0; i < groups.length; i++) {
788          var g = groups[i];
789          var items = g.items || [];
790          for (var j = 0; j < items.length; j++) {
791            var it = items[j];
792            var label = "";
793            var icon = "wifi";
794            if (it && typeof it === "object") {
795              label = String(it.label || "");
796              icon = it.icon || "wifi";
797            } else {
798              label = String(it || "");
799            }
800            ul.appendChild(
801              el("li", { class: "bw-amenity-item" }, [
802                el(
803                  "div",
804                  {
805                    class: "bw-amenity-row",
806                    title: label,
807                  },
808                  [
809                    el("i", { class: amenityIconClass(icon), "aria-hidden": "true" }),
810                    el("span", { class: "bw-amenity-cap" }, [label]),
811                  ],
812                ),
813              ]),
814            );
815          }
816        }
817        if (ul.childNodes.length) {
818          return el("div", { class: "bw-amenity-groups" }, [ul]);
819        }
820      }
821      var hl = r.amenity_highlights;
822      if (hl && hl.length) {
823        var tags = el("div", { class: "bw-tags" }, []);
824        for (var h = 0; h < hl.length; h++) {
825          tags.appendChild(el("span", { class: "bw-tag" }, [hl[h]]));
826        }
827        return tags;
828      }
829      return null;
830    }
831
832    function pluralNights(n) {
833      var k = n % 100;
834      var k1 = n % 10;
835      if (k >= 11 && k <= 14) return "ночей";
836      if (k1 === 1) return "ночь";
837      if (k1 >= 2 && k1 <= 4) return "ночи";
838      return "ночей";
839    }
840
841    function telHref(phone) {
842      var digits = String(phone || "").replace(/[^\d+]/g, "");
843      if (!digits) return "";
844      return "tel:" + digits;
845    }
846
847    function appendCardContactActions(actionsEl) {
848      var tel = telHref(state.contactPhone);
849      if (tel) {
850        actionsEl.appendChild(el("a", { class: "bw-cta bw-cta--secondary", href: tel }, ["Позвонить"]));
851      }
852      if (state.maxUrl) {
853        actionsEl.appendChild(
854          el(
855            "a",
856            {
857              class: "bw-cta bw-cta--max",
858              href: state.maxUrl,
859              target: "_blank",
860              rel: "noopener noreferrer",
861            },
862            ["Написать в MAX"],
863          ),
864        );
865      }
866    }
867
868    function fetchConfig() {
869      return fetch(
870        apiUrl("/api/v1/widget/config", {
871          organization_id: orgId,
872          token: token,
873        }),
874        { credentials: "omit" },
875      ).then(function (r) {
876        if (!r.ok) throw new Error("Виджет недоступен");
877        return r.json();
878      });
879    }
880
881    function guestEmailRequired() {
882      return !!(state.config && state.config.guest_email_required);
883    }
884
885    function isValidGuestEmail(s) {
886      var t = String(s || "").trim();
887      return t.length > 3 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t);
888    }
889
890    function formatIsoRu(iso) {
891      var m = String(iso || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
892      if (!m) return String(iso || "");
893      return m[3] + "." + m[2] + "." + m[1];
894    }
895
896    function nearbyOffsetLabel(offset) {
897      var d = Math.abs(Number(offset) || 0);
898      if (!d) return "исходные даты";
899      if (offset < 0) return "на " + d + " дн. раньше";
900      return "на " + d + " дн. позже";
901    }
902
903    function pluralCategories(n) {
904      var x = Math.abs(Number(n) || 0) % 100;
905      var n1 = x % 10;
906      if (x > 10 && x < 20) return "вариантов";
907      if (n1 > 1 && n1 < 5) return "варианта";
908      if (n1 === 1) return "вариант";
909      return "вариантов";
910    }
911
912    function buildSearchCriteriaFromInputs() {
913      var ci = qs("#bw-ci", formRoot).value;
914      var co = qs("#bw-co", formRoot).value;
915      var ad = qs("#bw-ad", formRoot).value;
916      var ch = qs("#bw-ch", formRoot).value;
917      return { check_in: ci, check_out: co, adults: ad, children: ch };
918    }
919
920    function isNearbyEnabled() {
921      return (
922        !!state.config &&
923        !!state.config.nearby_dates_enabled &&
924        Number(state.config.nearby_dates_range_days || 0) > 0
925      );
926    }
927
928    function applyNearbyWindow(win) {
929      if (!win) return;
930      var ci = String(win.check_in || "");
931      var co = String(win.check_out || "");
932      if (!ci || !co) return;
933      var ciEl = qs("#bw-ci", formRoot);
934      var coEl = qs("#bw-co", formRoot);
935      if (ciEl) ciEl.value = ci;
936      if (coEl) coEl.value = co;
937      syncDateInputs("ci");
938      state.lastSearch = {
939        check_in: ci,
940        check_out: co,
941        adults: state.lastSearch ? state.lastSearch.adults : qs("#bw-ad", formRoot).value,
942        children: state.lastSearch ? state.lastSearch.children : qs("#bw-ch", formRoot).value,
943      };
944      state.categories = win.categories || [];
945      state.transitionOptions = [];
946      state.selected = null;
947      state.selectedStay = null;
948      // Keep other nearby windows for context, but mark applied dates as main list.
949      renderList({ appliedOffset: Number(win.offset_days || 0) });
950      if (listHost && typeof listHost.scrollIntoView === "function") {
951        try {
952          listHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
953        } catch (e) {}
954      }
955    }
956
957    function renderNearbyAction(opts) {
958      nearbyActionHost.innerHTML = "";
959      if (!state.lastSearch) return;
960      var appliedOffset = opts && opts.appliedOffset != null ? Number(opts.appliedOffset) : null;
961      var windows = state.nearbyDateWindows || [];
962      var hasPrimary = !!(state.categories && state.categories.length);
963      var rangeDays = Math.max(0, Number((state.config && state.config.nearby_dates_range_days) || 5));
964      var showNearbyUi = isNearbyEnabled() || windows.length > 0;
965
966      if (!showNearbyUi) {
967        // Конфиг ещё не пришёл — не шумим; если включено, после загрузки config renderNearbyAction вызовут снова.
968        if (state.nearbyLoading && !state.config) {
969          nearbyActionHost.appendChild(
970            el("p", { class: "bw-nearby-hint muted" }, ["Подбираем соседние даты…"]),
971          );
972        }
973        return;
974      }
975
976      if (state.nearbyLoading) {
977        nearbyActionHost.appendChild(
978          el("p", { class: "bw-nearby-hint muted" }, [
979            "Подбираем варианты в диапазоне ±" + rangeDays + " дней…",
980          ]),
981        );
982        return;
983      }
984
985      if (!windows.length) {
986        if (!hasPrimary && isNearbyEnabled()) {
987          nearbyActionHost.appendChild(
988            el("p", { class: "bw-nearby-hint muted" }, [
989              "В диапазоне ±" + rangeDays + " дней свободных вариантов тоже не нашлось.",
990            ]),
991          );
992        }
993        return;
994      }
995
996      var hintText = hasPrimary
997        ? "Также есть варианты на соседние даты — можно сдвинуть заезд/выезд:"
998        : "На выбранные даты свободных номеров нет. Есть варианты на соседние даты:";
999      nearbyActionHost.appendChild(el("p", { class: "bw-nearby-hint" }, [hintText]));
1000
1001      var chips = el("div", { class: "bw-nearby-chips" }, []);
1002      windows.forEach(function (win) {
1003        var cats = win.categories || [];
1004        var count = cats.length;
1005        if (!count) return;
1006        var offset = Number(win.offset_days || 0);
1007        var label =
1008          formatIsoRu(win.check_in) +
1009          " – " +
1010          formatIsoRu(win.check_out) +
1011          " (" +
1012          nearbyOffsetLabel(offset) +
1013          ", " +
1014          count +
1015          " " +
1016          pluralCategories(count) +
1017          ")";
1018        var chipClass =
1019          "bw-nearby-chip" + (appliedOffset != null && appliedOffset === offset ? " bw-nearby-chip--active" : "");
1020        chips.appendChild(
1021          el(
1022            "button",
1023            {
1024              type: "button",
1025              class: chipClass,
1026              onclick: function () {
1027                applyNearbyWindow(win);
1028              },
1029            },
1030            [label],
1031          ),
1032        );
1033      });
1034      if (chips.childNodes.length) nearbyActionHost.appendChild(chips);
1035      nearbyActionHost.appendChild(
1036        el("span", { class: "bw-nearby-meta" }, [
1037          "Найдено дополнительных окон: " + windows.length + " (диапазон ±" + rangeDays + " дн.)",
1038        ]),
1039      );
1040    }
1041
1042    function requestAvailability(criteria, includeNearbyDates) {
1043      var q = {
1044        organization_id: orgId,
1045        token: token,
1046        check_in: criteria.check_in,
1047        check_out: criteria.check_out,
1048        adults: criteria.adults,
1049        children: criteria.children,
1050      };
1051      if (includeNearbyDates) q.include_nearby_dates = "true";
1052      return fetch(apiUrl("/api/v1/widget/availability", q), { credentials: "omit" })
1053        .then(function (r) {
1054          if (r.status === 429) throw new Error("Слишком много запросов");
1055          if (!r.ok) {
1056            return r.json().then(function (j) {
1057              var d = j && j.detail;
1058              var msg =
1059                typeof d === "string"
1060                  ? d
1061                  : d && d[0] && d[0].msg
1062                    ? d[0].msg
1063                    : "Ошибка поиска";
1064              throw new Error(msg);
1065            });
1066          }
1067          return r.json();
1068        });
1069    }
1070
1071    function runSearch() {
1072      setErr("");
1073      syncDateInputs("search");
1074      var c = buildSearchCriteriaFromInputs();
1075      var ci = c.check_in;
1076      var co = c.check_out;
1077      if (!ci || !co) {
1078        showSearchMessage("Укажите даты заезда и выезда.", true);
1079        return;
1080      }
1081      if (co <= ci) {
1082        showSearchMessage("Дата выезда должна быть позже даты заезда.", true);
1083        return;
1084      }
1085      state.lastSearch = c;
1086      state.nearbyDateWindows = [];
1087      state.transitionOptions = [];
1088      state.selected = null;
1089      state.selectedStay = null;
1090      setResultsPanelVisible(true);
1091      nearbyHost.innerHTML = "";
1092      listHost.innerHTML = "Загрузка…";
1093      // Всегда просим соседние даты: API вернёт их только если включено в настройках.
1094      state.nearbyLoading = true;
1095      renderNearbyAction();
1096
1097      requestAvailability(c, true)
1098        .then(function (data) {
1099          state.categories = data.categories || data.rooms || [];
1100          state.transitionOptions = data.transition_options || [];
1101          state.nearbyDateWindows = data.nearby_date_windows || [];
1102          state.nearbyLoading = false;
1103          renderList();
1104        })
1105        .catch(function (e) {
1106          state.nearbyDateWindows = [];
1107          state.nearbyLoading = false;
1108          showSearchMessage(searchErrorRu(e && e.message ? e.message : ""), true);
1109        })
1110        .finally(function () {
1111          state.nearbyLoading = false;
1112        });
1113    }
1114
1115    function renderCards(categories, stayInfo) {
1116      var wrap = el("div", { class: "bw-cards" });
1117      categories.forEach(function (r) {
1118        var items = galleryItemsForRoom(r);
1119        var previewIdx = 0;
1120        var card = el("div", { class: "bw-card" });
1121        var gal = el("div", { class: "bw-gal" });
1122        var galMain = el("div", { class: "bw-gal-main" });
1123        var galThumbs = el("div", { class: "bw-thumbs" });
1124
1125        function updateThumbStyles() {
1126          var ths = galThumbs.querySelectorAll(".bw-thumb");
1127          for (var t = 0; t < ths.length; t++) {
1128            ths[t].className = ths[t].className.replace(/\bbw-thumb--on\b/g, "").trim();
1129            if (t === previewIdx) ths[t].className = (ths[t].className + " bw-thumb--on").trim();
1130          }
1131        }
1132
1133        function syncPreview() {
1134          galMain.innerHTML = "";
1135          if (!items.length) {
1136            galMain.appendChild(
1137              el("div", { class: "muted", style: "padding:20px;text-align:center;font-size:12px" }, [
1138                "Нет фото и видео",
1139              ]),
1140            );
1141            return;
1142          }
1143          var it = items[previewIdx];
1144          if (it.kind === "image") {
1145            var img = el("img", { src: widgetAssetUrl(it.url), alt: "" });
1146            img.addEventListener("click", function (e) {
1147              e.stopPropagation();
1148              openLightbox(r, previewIdx);
1149            });
1150            galMain.appendChild(img);
1151          } else {
1152            var firstImg = r.media_urls && r.media_urls[0] ? widgetAssetUrl(r.media_urls[0]) : "";
1153            if (firstImg) {
1154              galMain.appendChild(
1155                el("img", {
1156                  src: firstImg,
1157                  alt: "",
1158                  style:
1159                    "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none",
1160                }),
1161              );
1162            } else {
1163              galMain.style.background = "linear-gradient(145deg,#1e293b,#0f172a)";
1164            }
1165            var ov = el("div", { class: "bw-vplay" });
1166            ov.appendChild(el("span", {}, ["▶"]));
1167            ov.addEventListener("click", function (e) {
1168              e.stopPropagation();
1169              openLightbox(r, previewIdx);
1170            });
1171            galMain.appendChild(ov);
1172          }
1173        }
1174
1175        items.forEach(function (it, gi) {
1176          var th = el("div", {
1177            class: "bw-thumb" + (gi === 0 ? " bw-thumb--on" : ""),
1178            onclick: function (e) {
1179              e.stopPropagation();
1180              previewIdx = gi;
1181              syncPreview();
1182              updateThumbStyles();
1183              if (it.kind === "video") {
1184                openLightbox(r, gi);
1185              }
1186            },
1187          });
1188          if (it.kind === "image") {
1189            th.appendChild(el("img", { src: widgetAssetUrl(it.url), alt: "" }));
1190          } else {
1191            th.className = (th.className + " bw-thumb--vid").trim();
1192            th.appendChild(document.createTextNode("▶"));
1193          }
1194          galThumbs.appendChild(th);
1195        });
1196
1197        syncPreview();
1198        gal.appendChild(galMain);
1199        if (items.length) gal.appendChild(galThumbs);
1200
1201        var body = el("div", { class: "bw-card-body" });
1202        body.appendChild(
1203          el("h3", { class: "bw-title" }, [
1204            r.public_name || (r.kind === "category" ? "Категория" : "Номер"),
1205          ]),
1206        );
1207
1208        var nights = r.stay_nights != null ? Number(r.stay_nights) : null;
1209        var total = fmtMoney(r.stay_total_cents, r.currency);
1210        var perNight = fmtMoney(r.base_rate_cents, r.currency);
1211        if (total && nights && nights > 0) {
1212          body.appendChild(
1213            el("div", { class: "bw-price" }, [
1214              total,
1215              el("span", { class: "bw-price-sub" }, [" за " + nights + " " + pluralNights(nights)]),
1216            ]),
1217          );
1218          if (perNight)
1219            body.appendChild(
1220              el("div", { class: "muted" }, [perNight + " × " + nights + " ноч."]),
1221            );
1222        } else if (perNight) {
1223          body.appendChild(el("div", { class: "bw-price" }, ["от " + perNight + " / ночь"]));
1224        }
1225
1226        var facts = [];
1227        if (r.floor != null && r.floor !== "") facts.push("этаж " + r.floor);
1228        var cap = r.max_guests != null ? r.max_guests : r.bed_count;
1229        if (cap != null && cap !== "") facts.push("до " + cap + " гостей");
1230        if (r.area_sqm != null && r.area_sqm !== "")
1231          facts.push(Math.round(r.area_sqm * 10) / 10 + " м²");
1232        if (facts.length) body.appendChild(el("div", { class: "bw-facts" }, [facts.join(" · ")]));
1233
1234        var amBlock = renderAmenitiesBlock(r);
1235        if (amBlock) body.appendChild(amBlock);
1236
1237        if (r.description)
1238          body.appendChild(el("div", { class: "bw-desc" }, [r.description]));
1239
1240        var cta = el(
1241          "button",
1242          {
1243            type: "button",
1244            class: "bw-cta",
1245            onclick: function (e) {
1246              e.stopPropagation();
1247              openRequestFor(r, card, stayInfo);
1248            },
1249          },
1250          ["Оставить заявку"],
1251        );
1252        var actions = el("div", { class: "bw-actions" }, [cta]);
1253        appendCardContactActions(actions);
1254        body.appendChild(actions);
1255
1256        var head = el("div", { class: "bw-card-head" });
1257        head.appendChild(gal);
1258        head.appendChild(body);
1259        card.appendChild(head);
1260
1261        card.addEventListener("click", function (e) {
1262          var t = e.target;
1263          if (t && typeof t.closest === "function" && t.closest(".bw-thumb,button,a,.bw-gal-main")) return;
1264          openRequestFor(r, card, stayInfo);
1265        });
1266
1267        wrap.appendChild(card);
1268      });
1269      return wrap;
1270    }
1271
1272    function renderTransitionCards(options, stayInfo) {
1273      var wrap = el("div", { class: "bw-cards" });
1274      (options || []).forEach(function (opt) {
1275        var segs = opt.segments || [];
1276        if (segs.length !== 2) return;
1277        var card = el("div", { class: "bw-card" });
1278        var body = el("div", { class: "bw-card-body", style: "flex:1 1 100%" });
1279        body.appendChild(el("div", { class: "bw-tr-badge" }, ["С переездом"]));
1280        body.appendChild(el("h3", { class: "bw-title" }, ["Два номера на период"]));
1281        segs.forEach(function (seg) {
1282          body.appendChild(
1283            el("div", { class: "bw-tr-seg" }, [
1284              el("strong", {}, [
1285                formatIsoRu(seg.check_in) + " – " + formatIsoRu(seg.check_out),
1286              ]),
1287              document.createTextNode(" · " + (seg.public_name || "Номер")),
1288            ]),
1289          );
1290        });
1291        var nights = opt.stay_nights != null ? Number(opt.stay_nights) : null;
1292        var total = fmtMoney(opt.stay_total_cents, opt.currency);
1293        if (total) {
1294          body.appendChild(
1295            el("div", { class: "bw-price" }, [
1296              total,
1297              nights && nights > 0
1298                ? el("span", { class: "bw-price-sub" }, [" за " + nights + " " + pluralNights(nights)])
1299                : null,
1300            ].filter(Boolean)),
1301          );
1302        }
1303        var selectable = {
1304          kind: "transition",
1305          id: opt.id,
1306          public_name: "С переездом",
1307          segments: segs,
1308          stay_total_cents: opt.stay_total_cents,
1309          currency: opt.currency,
1310          stay_nights: opt.stay_nights,
1311        };
1312        var cta = el(
1313          "button",
1314          {
1315            type: "button",
1316            class: "bw-cta",
1317            onclick: function (e) {
1318              e.stopPropagation();
1319              openRequestFor(selectable, card, stayInfo);
1320            },
1321          },
1322          ["Оставить заявку"],
1323        );
1324        var actions = el("div", { class: "bw-actions" }, [cta]);
1325        appendCardContactActions(actions);
1326        body.appendChild(actions);
1327        card.appendChild(body);
1328        card.addEventListener("click", function (e) {
1329          var t = e.target;
1330          if (t && typeof t.closest === "function" && t.closest("button,a")) return;
1331          openRequestFor(selectable, card, stayInfo);
1332        });
1333        wrap.appendChild(card);
1334      });
1335      return wrap;
1336    }
1337
1338    function renderList(opts) {
1339      listHost.innerHTML = "";
1340      nearbyHost.innerHTML = "";
1341      renderNearbyAction(opts);
1342      var baseStay = state.lastSearch
1343        ? {
1344            check_in: state.lastSearch.check_in,
1345            check_out: state.lastSearch.check_out,
1346            offset_days: 0,
1347          }
1348        : null;
1349      var hasTransitions = !!(state.transitionOptions && state.transitionOptions.length);
1350      if (!state.categories.length && !hasTransitions) {
1351        listHost.appendChild(
1352          el("p", { class: "muted" }, [
1353            state.nearbyDateWindows && state.nearbyDateWindows.length
1354              ? "На выбранные даты подходящих категорий нет — выберите соседние даты выше."
1355              : "Нет подходящих категорий на выбранные даты и состав гостей.",
1356          ]),
1357        );
1358      }
1359      if (state.categories.length) {
1360        listHost.appendChild(renderCards(state.categories, baseStay));
1361      } else if (hasTransitions) {
1362        var trStay = state.lastSearch
1363          ? { check_in: state.lastSearch.check_in, check_out: state.lastSearch.check_out, offset_days: 0 }
1364          : null;
1365        listHost.appendChild(
1366          el("p", { class: "muted" }, [
1367            "Непрерывного заселения на весь период нет — доступны варианты с одним переездом:",
1368          ]),
1369        );
1370        listHost.appendChild(renderTransitionCards(state.transitionOptions, trStay));
1371      }
1372      if (state.landing && state.landing.room && state.categories.length && !state.selected) {
1373        var wantRoom = String(state.landing.room);
1374        for (var ai = 0; ai < state.categories.length; ai++) {
1375          var cr = state.categories[ai];
1376          if (String(cr.room_category_id || cr.id) === wantRoom) {
1377            openRequestFor(cr, null, baseStay);
1378            break;
1379          }
1380        }
1381      }
1382      if (state.nearbyDateWindows && state.nearbyDateWindows.length) {
1383        var appliedOffset =
1384          opts && opts.appliedOffset != null ? Number(opts.appliedOffset) 
1384: null;
1385        state.nearbyDateWindows.forEach(function (win) {
1386          var stay = {
1387            check_in: String(win.check_in || ""),
1388            check_out: String(win.check_out || ""),
1389            offset_days: Number(win.offset_days || 0),
1390          };
1391          if (appliedOffset != null && stay.offset_days === appliedOffset) return;
1392          var cats = win.categories || [];
1393          if (!cats.length) return;
1394          var sec = el("div", { class: "bw-nearby-window", id: "bw-near-" + stay.offset_days }, []);
1395          sec.appendChild(
1396            el("h3", { class: "bw-nearby-title" }, [
1397              "Варианты " +
1398                nearbyOffsetLabel(stay.offset_days) +
1399                ": " +
1400                formatIsoRu(stay.check_in) +
1401                " – " +
1402                formatIsoRu(stay.check_out),
1403            ]),
1404          );
1405          sec.appendChild(
1406            el("p", { class: "muted bw-nearby-sub" }, [
1407              cats.length +
1408                " " +
1409                pluralCategories(cats.length) +
1410                ". Нажмите «Оставить заявку» — даты заезда/выезда подставятся автоматически.",
1411            ]),
1412          );
1413          sec.appendChild(renderCards(cats, stay));
1414          nearbyHost.appendChild(sec);
1415        });
1416      }
1417      if (
1418        !state.categories.length &&
1419        !hasTransitions &&
1420        (!state.nearbyDateWindows || !state.nearbyDateWindows.length)
1421      ) {
1422        reqHost.innerHTML = "";
1423      }
1424      reqHost.innerHTML = "";
1425    }
1426
1427    function renderForm() {
1428      reqHost.innerHTML = "";
1429      closeRequestModal();
1430      if (!state.selected) return;
1431      var r = state.selected;
1432      var overlay = el("div", {
1433        class: "bw-req-overlay",
1434        onclick: function (e) {
1435          if (e.target === overlay) closeRequestModal();
1436        },
1437      });
1438      var modal = el("div", { class: "bw-req-modal" });
1439      modal.appendChild(
1440        el(
1441          "button",
1442          { type: "button", class: "bw-req-close", onclick: closeRequestModal, "aria-label": "Закрыть" },
1443          ["×"],
1444        ),
1445      );
1446      var reqTitle =
1447        r.kind === "transition"
1448          ? "Заявка: вариант с переездом"
1449          : "Заявка: «" +
1450            (r.public_name || (r.kind === "category" ? "категория" : "номер")) +
1451            "»";
1452      modal.appendChild(el("h3", { style: "font-size:16px;margin:12px 0 8px" }, [reqTitle]));
1453      if (r.kind === "transition" && r.segments && r.segments.length) {
1454        r.segments.forEach(function (seg) {
1455          modal.appendChild(
1456            el("div", { class: "bw-tr-seg", style: "margin-bottom:6px" }, [
1457              formatIsoRu(seg.check_in) +
1458                " – " +
1459                formatIsoRu(seg.check_out) +
1460                " · " +
1461                (seg.public_name || "Номер"),
1462            ]),
1463          );
1464        });
1465      }
1466
1467      var gsurname = el("input", { type: "text", placeholder: "Фамилия", id: "bw-gs" });
1468      var gname = el("input", { type: "text", placeholder: "Имя", id: "bw-gf" });
1469      var phone = el("input", { type: "tel", placeholder: "+7 (___) ___-__-__", id: "bw-ph", inputmode: "tel" });
1470      gsurname.addEventListener("input", clearFieldValidation);
1471      gname.addEventListener("input", clearFieldValidation);
1472      phone.addEventListener("input", clearFieldValidation);
1473      function digitsOnly(v) {
1474        return String(v || "").replace(/\D/g, "");
1475      }
1476      function formatRuPhone(v) {
1477        var d = digitsOnly(v);
1478        if (!d) return "";
1479        if (d[0] === "8") d = "7" + d.slice(1);
1480        if (d[0] !== "7") d = "7" + d;
1481        d = d.slice(0, 11);
1482        var p = d.slice(1);
1483        var out = "+7";
1484        if (p.length) out += " (" + p.slice(0, 3);
1485        if (p.length >= 3) out += ")";
1486        if (p.length > 3) out += " " + p.slice(3, 6);
1487        if (p.length > 6) out += "-" + p.slice(6, 8);
1488        if (p.length > 8) out += "-" + p.slice(8, 10);
1489        return out;
1490      }
1491      phone.addEventListener("input", function () {
1492        phone.value = formatRuPhone(phone.value);
1493      });
1494      phone.addEventListener("focus", function () {
1495        if (!phone.value) phone.value = "+7 (";
1496      });
1497      var email = el("input", {
1498        type: "email",
1499        placeholder: guestEmailRequired() ? "Email *" : "Email (необязательно)",
1500        id: "bw-em",
1501      });
1502      if (guestEmailRequired()) email.required = true;
1503      var notes = el("textarea", { placeholder: "Комментарий", id: "bw-no", rows: "3", style: "width:100%;resize:vertical" });
1504
1505      modal.appendChild(
1506        el("div", { class: "row" }, [
1507          el("div", { class: "field", style: "flex:1;min-width:180px" }, [el("label", {}, ["Фамилия"]), gsurname]),
1508          el("div", { class: "field", style: "flex:1;min-width:180px" }, [el("label", {}, ["Имя"]), gname]),
1509          el("div", { class: "field", style: "flex:1;min-width:160px" }, [el("label", {}, ["Телефон"]), phone]),
1510        ]),
1511      );
1512      modal.appendChild(
1513        el("div", { class: "row" }, [el("div", { class: "field", style: "flex:1" }, [el("label", {}, ["Email"]), email])]),
1514      );
1515      modal.appendChild(
1516        el("div", { class: "row" }, [el("div", { class: "field", style: "flex:1" }, [el("label", {}, ["Комментарий"]), notes])]),
1517      );
1518
1519      var hp = el("input", { type: "text", name: "website", style: "position:absolute;left:-5000px", tabindex: "-1" });
1520      modal.appendChild(hp);
1521
1522      var capDiv = el("div", { id: "bw-captcha" });
1523      modal.appendChild(capDiv);
1524
1525      appendPdConsentAndSubmit(modal, {
1526        consentId: "bw-pd-consent",
1527        termsId: "bw-guest-terms",
1528        submitId: "bw-submit",
1529        style: "margin-top:10px",
1530        legalBaseUrl: opts.legalBaseUrl,
1531        onSubmit: function () {
1532          void submitReq();
1533        },
1534      });
1535      overlay.appendChild(modal);
1536      requestModalEl = overlay;
1537      resultsRoot.appendChild(overlay);
1538
1539      state.captchaToken = null;
1540
1541      if (state.config && state.config.captcha_enabled) {
1542        var provider = String(state.config.captcha_provider || "altcha").toLowerCase();
1543        if (provider === "turnstile" && state.config.captcha_site_key) {
1544          loadTurnstile(function () {
1545            if (!global.turnstile) return;
1546            capDiv.innerHTML = "";
1547            global.turnstile.render(capDiv, {
1548              sitekey: state.config.captcha_site_key,
1549              callback: function (tok) {
1550                state.captchaToken = tok;
1551              },
1552              "expired-callback": function () {
1553                state.captchaToken = null;
1554              },
1555            });
1556          });
1557        } else if (provider === "altcha") {
1558          capDiv.innerHTML = "";
1559          var box = el("div", { class: "row", style: "margin-bottom:0" });
1560          var tokenPart = "";
1561          var qText = el("div", { class: "bw-captcha-q" }, ["Проверка..."]);
1562          var ans = el("input", { type: "text", inputmode: "numeric", placeholder: "Ответ", style: "max-width:120px" });
1563          ans.addEventListener("input", function () {
1564            var v = String(ans.value || "").trim();
1565            state.captchaToken = tokenPart && v ? tokenPart + ":" + v : null;
1566          });
1567          box.appendChild(el("div", { class: "bw-captcha-row" }, [qText, ans]));
1568          capDiv.appendChild(box);
1569          fetch(
1570            apiUrl("/api/v1/widget/captcha/challenge", {
1571              organization_id: orgId,
1572              token: token,
1573            }),
1574            { credentials: "omit" },
1575          )
1576            .then(function (r) {
1577              if (!r.ok) throw new Error("Капча недоступна");
1578              return r.json();
1579            })
1580            .then(function (j) {
1581              tokenPart = String((j && j.token) || "");
1582              qText.textContent = String((j && j.question) || "Решите пример");
1583              state.captchaToken = null;
1584            })
1585            .catch(function () {
1586              qText.textContent = "Не удалось загрузить капчу";
1587              state.captchaToken = null;
1588            });
1589        }
1590      }
1591    }
1592
1593    function loadTurnstile(cb) {
1594      if (global.turnstile) return cb();
1595      var s = document.createElement("script");
1596      s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
1597      s.async = true;
1598      s.onload = cb;
1599      document.head.appendChild(s);
1600    }
1601
1602    function submitReq() {
1603      setErr("");
1604      if (!state.selected) return;
1605      var selectedStay = state.selectedStay || {};
1606      var ci = selectedStay.check_in || qs("#bw-ci", formRoot).value;
1607      var co = selectedStay.check_out || qs("#bw-co", formRoot).value;
1608      var ad = parseInt(qs("#bw-ad", formRoot).value, 10) || 1;
1609      var ch = parseInt(qs("#bw-ch", formRoot).value, 10) || 0;
1610      var gs = (qs("#bw-gs", resultsRoot) && qs("#bw-gs", resultsRoot).value) || "";
1611      var gf = (qs("#bw-gf", resultsRoot) && qs("#bw-gf", resultsRoot).value) || "";
1612      var ph = (qs("#bw-ph", resultsRoot) && qs("#bw-ph", resultsRoot).value) || "";
1613      var em = (qs("#bw-em", resultsRoot) && qs("#bw-em", resultsRoot).value) || "";
1614      var no = (qs("#bw-no", resultsRoot) && qs("#bw-no", resultsRoot).value) || "";
1615      var website =
1616        (qs('input[name="website"]', resultsRoot) && qs('input[name="website"]', resultsRoot).value) || "";
1617
1618      clearFieldValidation();
1619      var hasMissing = false;
1620      if (!gs.trim()) {
1621        markRequiredField(qs("#bw-gs", resultsRoot));
1622        hasMissing = true;
1623      }
1624      if (!gf.trim()) {
1625        markRequiredField(qs("#bw-gf", resultsRoot));
1626        hasMissing = true;
1627      }
1628      if (!ph.trim()) {
1629        markRequiredField(qs("#bw-ph", resultsRoot));
1630        hasMissing = true;
1631      }
1632      if (guestEmailRequired() && !isValidGuestEmail(em)) {
1633        markRequiredField(qs("#bw-em", resultsRoot));
1634        hasMissing = true;
1635      }
1636      if (hasMissing) {
1637        setErr("Заполните обязательные поля");
1638        return;
1639      }
1640      var consent = qs("#bw-pd-consent", resultsRoot);
1641      var terms = qs("#bw-guest-terms", resultsRoot);
1642      if (!consent || !consent.checked || !terms || !terms.checked) {
1643        setErr("Примите соглашение модуля и дайте согласие на обработку персональных данных");
1644        return;
1645      }
1646      if (
1647        state.config &&
1648        state.config.captcha_enabled &&
1649        String(state.config.captcha_provider || "altcha").toLowerCase() !== "none" &&
1650        !state.captchaToken
1651      ) {
1652        setErr("Пройдите проверку капчи");
1653        return;
1654      }
1655
1656      var body = {
1657        check_in: ci,
1658        check_out: co,
1659        adults: ad,
1660        children: ch,
1661        guest_name: (gs.trim() + " " + gf.trim()).trim(),
1662        guest_surname: gs.trim(),
1663        guest_first_name: gf.trim(),
1664        phone: ph.trim(),
1665        email: em.trim() || null,
1666        notes: no.trim() || null,
1667        captcha_token: state.captchaToken || null,
1668        website: website || null,
1669        pd_consent: true,
1670        guest_terms_accepted: true,
1671      };
1672      if (state.selected.kind === "transition" && state.selected.segments) {
1673        body.stay_segments = state.selected.segments.map(function (seg) {
1674          var s = {
1675            check_in: seg.check_in,
1676            check_out: seg.check_out,
1677          };
1678          if (seg.kind === "room" && seg.room_id) s.room_id = seg.room_id;
1679          else if (seg.room_category_id) s.room_category_id = seg.room_category_id;
1680          else if (seg.room_id) s.room_id = seg.room_id;
1681          return s;
1682        });
1683      } else if (state.selected.kind === "room") {
1684        body.room_id = state.selected.id;
1685      } else {
1686        body.room_category_id = state.selected.id;
1687      }
1688      if (state.landing) {
1689        var offerKey =
1690          (state.resolvedOffer && state.resolvedOffer.offer_id) || state.landing.offer || "";
1691        if (offerKey) body.offer_id = offerKey;
1692        ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "yclid"].forEach(
1693          function (k) {
1694            if (state.landing[k]) body[k] = state.landing[k];
1695          },
1696        );
1697      }
1698
1699      var btn = qs("#bw-submit", resultsRoot);
1700      if (btn) btn.disabled = true;
1701      fetch(
1702        apiUrl("/api/v1/widget/requests", {
1703          organization_id: orgId,
1704          token: token,
1705        }),
1706        {
1707          method: "POST",
1708          credentials: "omit",
1709          headers: { "Content-Type": "application/json" },
1710          body: JSON.stringify(body),
1711        },
1712      )
1713        .then(function (r) {
1714          if (r.status === 429) throw new Error("Слишком много запросов или дубликат заявки");
1715          if (!r.ok)
1716            return r.json().then(function (j) {
1717              var d = j.detail;
1718              var msg =
1719                typeof d === "string"
1720                  ? d
1721                  : d && d[0] && d[0].msg
1722                    ? d[0].msg
1723                    : "Ошибка";
1724              throw new Error(msg);
1725            });
1726          return r.json();
1727        })
1728        .then(function (res) {
1729          reqHost.innerHTML = "";
1730          closeRequestModal();
1731          listHost.innerHTML = "";
1732          setErr("");
1733          var offerId =
1734            (state.resolvedOffer && state.resolvedOffer.offer_id) ||
1735            (state.landing && state.landing.offer) ||
1736            "";
1737          if (offerId) {
1738            pushEcommerce("add", {
1739              id: offerId,
1740              name: (state.selected && state.selected.public_name) || "booking",
1741              price: (state.selected && state.selected.stay_total_cents) / 100 || 0,
1742              quantity: 1,
1743            });
1744          }
1745          pushMetrikaGoal("booking_request");
1746          resultsRoot.appendChild(el("p", { style: "color:#16a34a" }, ["Заявка отправлена. Мы свяжемся с вами."]));
1747        })
1748        .catch(function (e) {
1749          setErr(e.message || "Ошибка");
1750        })
1751        .finally(function () {
1752          syncSubmitWithPdConsent(
1753            qs("#bw-submit", resultsRoot),
1754            qs("#bw-pd-consent", resultsRoot),
1755            qs("#bw-guest-terms", resultsRoot),
1756          );
1757        });
1758    }
1759
1760    function applyLandingToForm() {
1761      var L = state.landing;
1762      if (!L) return;
1763      var ciEl = qs("#bw-ci", formRoot);
1764      var coEl = qs("#bw-co", formRoot);
1765      var adEl = qs("#bw-ad", formRoot);
1766      if (L.in && ciEl) ciEl.value = L.in;
1767      if (L.out && coEl) coEl.value = L.out;
1768      if (L.adults && adEl) adEl.value = L.adults;
1769    }
1770
1771    function resolveOfferThenSearch() {
1772      var L = state.landing;
1773      if (!L || !L.offer) {
1774        if (L && L.in && L.out) runSearch();
1775        return;
1776      }
1777      fetch(
1778        apiUrl("/api/v1/widget/offer-resolve", {
1779          organization_id: orgId,
1780          token: token,
1781          offer: L.offer,
1782          adults: L.adults || undefined,
1783        }),
1784        { credentials: "omit" },
1785      )
1786        .then(function (r) {
1787          if (!r.ok) throw new Error("offer");
1788          return r.json();
1789        })
1790        .then(function (data) {
1791          state.resolvedOffer = data;
1792          if (data.available) {
1793            if (!L.in && data.check_in) L.in = data.check_in;
1794            if (!L.out && data.check_out) L.out = data.check_out;
1795            if (!L.room && data.room_category_id) L.room = data.room_category_id;
1796            applyLandingToForm();
1797            pushMetrikaGoal("offer_landing_view");
1798            if (data.offer_id) {
1799              pushEcommerce("detail", {
1800                id: data.offer_id,
1801                name: data.category_name || "offer",
1802                price: data.price || 0,
1803                quantity: 1,
1804              });
1805            }
1806            runSearch();
1807          } else {
1808            pushMetrikaGoal("offer_unavailable");
1809            showSearchMessage(
1810              "Выбранное предложение больше недоступно. Попробуйте другие даты ниже.",
1811              true,
1812            );
1813            if (data.nearby_date_windows && data.nearby_date_windows.length) {
1814              renderOfferNearbyAlternatives(data.nearby_date_windows);
1815            } else if (L.in && L.out) {
1816              runSearch();
1817            }
1818          }
1819        })
1820        .catch(function () {
1821          applyLandingToForm();
1822          if (L.in && L.out) runSearch();
1823        });
1824    }
1825
1826    fetchConfig()
1827      .then(function (cfg) {
1828        state.config = cfg;
1829        state.contactPhone = String((cfg && cfg.contact_phone) || "").trim();
1830        state.maxUrl = String((cfg && cfg.max_url) || "").trim();
1831        renderNearbyAction();
1832        applyLandingToForm();
1833        if (
1834          state.landing &&
1835          (state.landing.offer || (state.landing.in && state.landing.out))
1836        ) {
1837          resolveOfferThenSearch();
1838        }
1839      })
1840      .catch(function () {
1841        setErr("Не удалось загрузить настройки виджета");
1842      });
1843  }
1844
1845  function initHotOffers(opts) {
1846    var apiBase = (opts.apiBaseUrl || "").replace(/\/$/, "");
1847    var orgId = opts.organizationId || "";
1848    var token = opts.token || "";
1849    var layoutOpt = String(opts.layout || "").toLowerCase();
1850    var root = typeof opts.mount === "string" ? qs(opts.mount) : opts.mount;
1851    if (!root) return;
1852    if (!apiBase || !orgId || !token) {
1853      root.innerHTML = "";
1854      root.appendChild(el("p", { class: "booking-widget-error" }, ["Нужны apiBaseUrl, organizationId и token"]));
1855      return;
1856    }
1857
1858    root.innerHTML = "";
1859    root.className = (root.className ? root.className + " " : "") + "booking-widget-root bw-hot-root";
1860
1861    var state = {
1862      config: null,
1863      items: [],
1864      layout: layoutOpt === "vertical" || layoutOpt === "tiles" ? layoutOpt : "horizontal",
1865      contactPhone: "",
1866      maxUrl: "",
1867      photoClickUrl: "",
1868      selected: null,
1869      captchaToken: null,
1870    };
1871
1872    function guestEmailRequired() {
1873      return !!(state.config && state.config.guest_email_required);
1874    }
1875
1876    function isValidGuestEmail(s) {
1877      var t = String(s || "").trim();
1878      return t.length > 3 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t);
1879    }
1880
1881    var style = el("style", {}, []);
1882    style.textContent =
1883      ".bw-hot-root{font-family:system-ui,-apple-system,sans-serif;font-size:15px;line-height:1.45;color:#0f172a;width:100%;box-sizing:border-box}" +
1884      ".bw-hot-root *{box-sizing:border-box}" +
1885      ".bw-hot-title{margin:0 0 14px;font-size:22px;font-weight:700;color:#0f172a}" +
1886      ".bw-hot-msg{margin:8px 0;padding:12px 14px;border-radius:12px;border:1px solid #e2e8f0;background:#f8fafc;color:#334155;font-size:14px}" +
1887      ".bw-hot-msg--err{border-color:#fecaca;background:#fef2f2;color:#991b1b}" +
1888      ".bw-hot-track{display:flex;gap:14px;overflow-x:auto;padding:6px 12px 16px;scroll-snap-type:x mandatory;scroll-padding-inline:12px;-webkit-overflow-scrolling:touch}" +
1889      ".bw-hot-track--vertical{flex-direction:column;overflow-x:visible;overflow-y:auto;max-height:960px;scroll-snap-type:y mandatory;padding:0;width:100%;gap:12px}" +
1890      ".bw-hot-track--tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;overflow:visible;scroll-snap-type:none;padding:6px 0 12px}" +
1891      ".bw-hot-card{position:relative;flex:0 0 min(420px,calc(100% - 40px));width:min(420px,calc(100% - 40px));scroll-snap-align:start;scroll-snap-stop:always;border:1px solid #e2e8f0;border-radius:16px;background:#fff;box-shadow:0 3px 12px rgba(15,23,42,.1);overflow:hidden;display:flex;flex-direction:column}" +
1892      ".bw-hot-track--vertical .bw-hot-card{flex:0 0 auto;width:100%;max-width:none}" +
1893      ".bw-hot-track--vertical .bw-hot-photo{min-height:0;aspect-ratio:4/3}" +
1894      ".bw-hot-track--vertical .bw-hot-actions{flex-direction:column}" +
1895      ".bw-hot-track--vertical .bw-hot-cta{flex:1 1 auto;width:100%}" +
1896      ".bw-hot-track--tiles .bw-hot-card{flex:initial;width:auto}" +
1897      ".bw-hot-photo{position:relative;aspect-ratio:4/3;min-height:210px;background:#f1f5f9;overflow:hidden}" +
1898      "a.bw-hot-photo{display:block;color:inherit;text-decoration:none;cursor:pointer}" +
1899      ".bw-hot-photo img{width:100%;height:100%;object-fit:cover;display:block}" +
1900      ".bw-hot-photo-overlay{position:absolute;inset:0;padding:16px;background:linear-gradient(180deg,rgba(15,23,42,.72),rgba(15,23,42,.9));color:#fff;opacity:0;pointer-events:none;transition:opacity .18s ease;overflow:auto;z-index:3}" +
1901      ".bw-hot-photo:hover .bw-hot-photo-overlay,.bw-hot-photo:focus-within .bw-hot-photo-overlay,.bw-hot-photo.bw-hot-photo--open .bw-hot-photo-overlay{opacity:1;pointer-events:auto}" +
1902      ".bw-hot-ov-title{font-size:16px;font-weight:700;margin:0 0 10px}" +
1903      ".bw-hot-ov-facts{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 10px}" +
1904      ".bw-hot-ov-chip{font-size:13px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:4px 10px}" +
1905      ".bw-hot-ov-desc{font-size:14px;line-height:1.4;margin:0 0 10px;opacity:.95;display:-webkit-box;-webkit-line-clamp:8;-webkit-box-orient:vertical;overflow:hidden}" +
1906      ".bw-hot-ov-tags{display:flex;flex-wrap:wrap;gap:6px}" +
1907      ".bw-hot-ov-tag{font-size:12px;background:rgba(2,132,199,.35);border-radius:8px;padding:3px 9px}" +
1908      ".bw-hot-badge{position:absolute;top:12px;left:12px;z-index:2;background:linear-gradient(135deg,#ef4444,#f97316);color:#fff;font-weight:800;font-size:18px;padding:8px 12px;border-radius:999px;box-shadow:0 4px 12px rgba(239,68,68,.45);letter-spacing:.02em}" +
1909      ".bw-hot-body{padding:16px;display:flex;flex-direction:column;gap:8px;flex:1}" +
1910      ".bw-hot-name{font-size:18px;font-weight:650;margin:0;color:#0f172a}" +
1911      ".bw-hot-meta{font-size:14px;color:#475569;margin:0}" +
1912      ".bw-hot-dates{font-size:14px;color:#64748b;margin:0}" +
1913      ".bw-hot-prices{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;margin-top:4px}" +
1914      ".bw-hot-old{font-size:15px;color:#94a3b8;text-decoration:line-through}" +
1915      ".bw-hot-new{font-size:26px;font-weight:800;color:#dc2626}" +
1916      ".bw-hot-new small{font-size:13px;font-weight:600;color:#b91c1c;margin-left:2px}" +
1917      ".bw-hot-actions{margin-top:auto;display:flex;flex-wrap:wrap;gap:8px;align-items:stretch}" +
1918      ".bw-hot-cta{flex:1 1 120px;background:#0284c7;color:#fff;border:1px solid #0284c7;border-radius:10px;padding:10px 12px;cursor:pointer;font-weight:600;font-size:14px;text-align:center;text-decoration:none;display:inline-flex;align-items:center;justify-content:center}" +
1919      ".bw-hot-cta:hover{filter:brightness(1.05)}" +
1920      ".bw-hot-cta:disabled{opacity:.55;cursor:not-allowed}" +
1921      ".bw-hot-cta--secondary{background:#fff;color:#0f172a;border-color:#cbd5e1;font-weight:600}" +
1922      ".bw-hot-cta--secondary:hover{background:#f8fafc;filter:none}" +
1923      ".bw-hot-cta--max{background:#1a1a2e;color:#fff;border-color:#1a1a2e}" +
1924      ".bw-hot-cta--max:hover{filter:brightness(1.15)}" +
1925      "@media (max-width:640px){" +
1926      ".bw-hot-track:not(.bw-hot-track--vertical):not(.bw-hot-track--tiles){gap:12px;padding:4px 10px 18px;scroll-padding-inline:10px}" +
1927      ".bw-hot-track:not(.bw-hot-track--vertical):not(.bw-hot-track--tiles) .bw-hot-card{flex:0 0 calc(100% - 28px);width:calc(100% - 28px);max-width:none}" +
1928      ".bw-hot-photo{min-height:240px}" +
1929      ".bw-hot-ov-desc{-webkit-line-clamp:10}" +
1930      "}" +
1931      ".bw-hot-root .bw-req-overlay{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:99998;display:flex;align-items:center;justify-content:center;padding:16px}" +
1932      ".bw-hot-root .bw-req-modal{width:min(560px,96vw);max-height:88vh;overflow:auto;background:#fff;border-radius:14px;padding:16px;box-shadow:0 14px 40px rgba(2,6,23,.35);position:relative}" +
1933      ".bw-hot-root .bw-req-close{position:absolute;top:8px;right:8px;width:36px;height:36px;border-radius:999px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;font-size:22px;line-height:1}" +
1934      ".bw-hot-root .row{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;margin-bottom:12px}" +
1935      ".bw-hot-root .field{display:flex;flex-direction:column;gap:4px;flex:1 1 160px}" +
1936      ".bw-hot-root input,.bw-hot-root select,.bw-hot-root textarea,.bw-hot-root button{border-radius:10px;border:1px solid #cbd5e1;padding:10px 12px;font-size:15px}" +
1937      ".bw-hot-root button.primary{background:#0284c7;color:#fff;border-color:#0284c7;cursor:pointer}" +
1938      ".bw-hot-root button.primary:disabled{opacity:.55;cursor:not-allowed}" +
1939      ".bw-hot-root .bw-consent{display:flex;align-items:flex-start;gap:8px;margin:12px 0 4px;font-size:13px;color:#334155;cursor:pointer;line-height:1.35}" +
1940      ".bw-hot-root .bw-consent input{margin:2px 0 0;flex-shrink:0;width:16px;height:16px;accent-color:#0284c7}" +
1941      ".bw-hot-root .muted{color:#64748b;font-size:13px}" +
1942      ".bw-hot-root .bw-invalid{border-color:#ef4444 !important;box-shadow:0 0 0 2px rgba(239,68,68,.12)}" +
1943      ".bw-hot-root .bw-field-err{color:#b91c1c;font-size:13px}";
1944    root.appendChild(style);
1945
1946    var title = el("h3", { class: "bw-hot-title" }, ["Горящие предложения"]);
1947    root.appendChild(title);
1948    var msgBox = el("div", { class: "muted" });
1949    root.appendChild(msgBox);
1950    var listHost = el("div", {}, []);
1951    root.appendChild(listHost);
1952    var reqHost = el("div", {}, []);
1953    root.appendChild(reqHost);
1954
1955    function setMsg(t, isErr) {
1956      msgBox.className = t ? "bw-hot-msg" + (isErr ? " bw-hot-msg--err" : "") : "muted";
1957      msgBox.textContent = t || "";
1958    }
1959
1960    function apiUrl(path, q) {
1961      var p = path.charAt(0) === "/" ? path : "/" + path;
1962      var u = new URL(apiBase + p);
1963      Object.keys(q || {}).forEach(function (k) {
1964        if (q[k] != null && q[k] !== "") u.searchParams.set(k, String(q[k]));
1965      });
1966      return u.toString();
1967    }
1968
1969    function widgetAssetUrl(path) {
1970      if (!path) return "";
1971      if (/^https?:\/\//i.test(path)) {
1972        try {
1973          var u = new URL(path);
1974          if (u.pathname.indexOf("/api/v1/widget/") === 0) {
1975            u.searchParams.set("organization_id", orgId);
1976            u.searchParams.set("token", token);
1977          }
1978          return u.toString();
1979        } catch (e) {
1980          return path;
1981        }
1982      }
1983      return apiUrl(path, { organization_id: orgId, token: token });
1984    }
1985
1986    function formatMoney(cents) {
1987      var rub = Math.max(0, Math.round(Number(cents || 0) / 100));
1988      return String(rub).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " ₽";
1989    }
1990
1991    function formatIsoRu(iso) {
1992      if (!iso) return "";
1993      var p = String(iso).split("-");
1994      if (p.length !== 3) return String(iso);
1995      return p[2] + "." + p[1] + "." + p[0];
1996    }
1997
1998    function nightsWord(n) {
1999      var x = Math.abs(n) % 100;
2000      var y = x % 10;
2001      if (x > 10 && x < 20) return n + " ночей";
2002      if (y === 1) return n + " ночь";
2003      if (y >= 2 && y <= 4) return n + " ночи";
2004      return n + " ночей";
2005    }
2006
2007    function daysWord(n) {
2008      var x = Math.abs(n) % 100;
2009      var y = x % 10;
2010      if (x > 10 && x < 20) return n + " суток";
2011      if (y === 1) return n + " сутки";
2012      return n + " суток";
2013    }
2014
2015    function freeNightsLabel(item) {
2016      var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 0;
2017      var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin;
2018      if (nmin < 1) nmin = 1;
2019      if (nmax < nmin) nmax = nmin;
2020      if (nmin === nmax) return "свободно " + daysWord(nmin);
2021      return "свободно " + nmin + "–" + nmax + " суток";
2022    }
2023
2024    function addDaysIso(iso, days) {
2025      var p = String(iso || "").split("-");
2026      if (p.length !== 3) return iso;
2027      var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
2028      d.setDate(d.getDate() + days);
2029      var y = d.getFullYear();
2030      var m = String(d.getMonth() + 1).padStart(2, "0");
2031      var dd = String(d.getDate()).padStart(2, "0");
2032      return y + "-" + m + "-" + dd;
2033    }
2034
2035    function telHref(phone) {
2036      var digits = String(phone || "").replace(/[^\d+]/g, "");
2037      if (!digits) return "";
2038      return "tel:" + digits;
2039    }
2040
2041    function closeRequestModal() {
2042      reqHost.innerHTML = "";
2043      state.selected = null;
2044      state.captchaToken = null;
2045    }
2046
2047    function openRequestModal(item) {
2048      state.selected = item;
2049      state.captchaToken = null;
2050      reqHost.innerHTML = "";
2051      var overlay = el("div", {
2052        class: "bw-req-overlay",
2053        onclick: function (ev) {
2054          if (ev.target === overlay) closeRequestModal();
2055        },
2056      });
2057      var modal = el("div", { class: "bw-req-modal" }, []);
2058      modal.appendChild(
2059        el(
2060          "button",
2061          {
2062            type: "button",
2063            class: "bw-req-close",
2064            onclick: function () {
2065              closeRequestModal();
2066            },
2067          },
2068          ["×"],
2069        ),
2070      );
2071      modal.appendChild(el("h3", { style: "margin:0 0 8px;font-size:16px" }, ["Заявка на бронирование"]));
2072      modal.appendChild(
2073        el("p", { class: "muted", style: "margin:0 0 10px" }, [
2074          item.public_name +
2075            " · заезд " +
2076            formatIsoRu(item.check_in) +
2077            " · " +
2078            freeNightsLabel(item) +
2079            " · от " +
2080            formatMoney(item.price_cents) +
2081            " (−" +
2082            item.discount_percent +
2083            "%)",
2084        ]),
2085      );
2086      var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 1;
2087      var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin;
2088      if (nmax < nmin) nmax = nmin;
2089      modal.appendChild(
2090        el("div", { class: "row" }, [
2091          el("div", { class: "field" }, [
2092            el("label", {}, ["Срок проживания"]),
2093            (function () {
2094              var s = el("select", { id: "bw-ho-nights" });
2095              for (var n = nmin; n <= nmax; n++) {
2096                var opt = el("option", { value: String(n) }, [daysWord(n)]);
2097                if (n === nmin) opt.selected = true;
2098                s.appendChild(opt);
2099              }
2100              return s;
2101            })(),
2102          ]),
2103        ]),
2104      );
2105      modal.appendChild(
2106        el("div", { class: "row" }, [
2107          el("div", { class: "field" }, [
2108            el("label", {}, ["Фамилия *"]),
2109            el("input", { type: "text", id: "bw-ho-gs", autocomplete: "family-name" }),
2110          ]),
2111          el("div", { class: "field" }, [
2112            el("label", {}, ["Имя *"]),
2113            el("input", { type: "text", id: "bw-ho-gf", autocomplete: "given-name" }),
2114          ]),
2115        ]),
2116      );
2117      modal.appendChild(
2118        el("div", { class: "row" }, [
2119          el("div", { class: "field" }, [
2120            el("label", {}, ["Телефон *"]),
2121            el("input", { type: "tel", id: "bw-ho-ph", autocomplete: "tel" }),
2122          ]),
2123          el("div", { class: "field" }, [
2124            el("label", {}, [guestEmailRequired() ? "Email *" : "Email"]),
2125            el("input", {
2126              type: "email",
2127              id: "bw-ho-em",
2128              autocomplete: "email",
2129              required: guestEmailRequired() ? true : undefined,
2130            }),
2131          ]),
2132        ]),
2133      );
2134      modal.appendChild(
2135        el("div", { class: "row" }, [
2136          el("div", { class: "field" }, [
2137            el("label", {}, ["Взрослых"]),
2138            (function () {
2139              var s = el("select", { id: "bw-ho-ad" });
2140              for (var a = 1; a <= 10; a++) s.appendChild(el("option", { value: String(a) }, [String(a)]));
2141              return s;
2142            })(),
2143          ]),
2144          el("div", { class: "field" }, [
2145            el("label", {}, ["Детей"]),
2146            (function () {
2147              var s = el("select", { id: "bw-ho-ch" });
2148              for (var c = 0; c <= 10; c++) s.appendChild(el("option", { value: String(c) }, [String(c)]));
2149              return s;
2150            })(),
2151          ]),
2152        ]),
2153      );
2154      modal.appendChild(
2155        el("div", { class: "field", style: "margin-bottom:10px" }, [
2156          el("label", {}, ["Комментарий"]),
2157          el("textarea", { id: "bw-ho-no", rows: "2" }),
2158        ]),
2159      );
2160      modal.appendChild(el("input", { type: "text", name: "website", tabindex: "-1", autocomplete: "off", style: "position:absolute;left:-9999px;opacity:0", "aria-hidden": "true" }));
2161      var capDiv = el("div", { id: "bw-ho-cap", style: "margin-bottom:10px" });
2162      modal.appendChild(capDiv);
2163      var errLine = el("p", { class: "bw-field-err", id: "bw-ho-err" });
2164      modal.appendChild(errLine);
2165      appendPdConsentAndSubmit(modal, {
2166        consentId: "bw-ho-pd-consent",
2167        termsId: "bw-ho-guest-terms",
2168        submitId: "bw-ho-submit",
2169        legalBaseUrl: opts.legalBaseUrl,
2170        onSubmit: function () {
2171          void submitHotOffer();
2172        },
2173      });
2174      overlay.appendChild(modal);
2175      reqHost.appendChild(overlay);
2176
2177      if (
2178        state.config &&
2179        state.config.captcha_enabled &&
2180        String(state.config.captcha_provider || "altcha").toLowerCase() === "altcha"
2181      ) {
2182        var tokenPart = "";
2183        var qText = el("div", { class: "muted" }, ["Проверка..."]);
2184        var ans = el("input", { type: "text", inputmode: "numeric", placeholder: "Ответ", style: "max-width:120px" });
2185        ans.addEventListener("input", function () {
2186          var v = String(ans.value || "").trim();
2187          state.captchaToken = tokenPart && v ? tokenPart + ":" + v : null;
2188        });
2189        capDiv.appendChild(el("div", { class: "row", style: "align-items:center" }, [qText, ans]));
2190        fetch(apiUrl("/api/v1/widget/captcha/challenge", { organization_id: orgId, token: token }), { credentials: "omit" })
2191          .then(function (r) {
2192            if (!r.ok) throw new Error("Капча недоступна");
2193            return r.json();
2194          })
2195          .then(function (j) {
2196            tokenPart = String((j && j.token) || "");
2197            qText.textContent = String((j && j.question) || "Решите пример");
2198            state.captchaToken = null;
2199          })
2200          .catch(function () {
2201            qText.textContent = "Не удалось загрузить капчу";
2202            state.captchaToken = null;
2203          });
2204      }
2205    }
2206
2207    function submitHotOffer() {
2208      var item = state.selected;
2209      if (!item) return;
2210      var gs = (qs("#bw-ho-gs", reqHost) && qs("#bw-ho-gs", reqHost).value) || "";
2211      var gf = (qs("#bw-ho-gf", reqHost) && qs("#bw-ho-gf", reqHost).value) || "";
2212      var ph = (qs("#bw-ho-ph", reqHost) && qs("#bw-ho-ph", reqHost).value) || "";
2213      var em = (qs("#bw-ho-em", reqHost) && qs("#bw-ho-em", reqHost).value) || "";
2214      var no = (qs("#bw-ho-no", reqHost) && qs("#bw-ho-no", reqHost).value) || "";
2215      var ad = parseInt((qs("#bw-ho-ad", reqHost) && qs("#bw-ho-ad", reqHost).value) || "1", 10) || 1;
2216      var ch = parseInt((qs("#bw-ho-ch", reqHost) && qs("#bw-ho-ch", reqHost).value) || "0", 10) || 0;
2217      var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 1;
2218      var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin;
2219      var nightsSel = parseInt((qs("#bw-ho-nights", reqHost) && qs("#bw-ho-nights", reqHost).value) || String(nmin), 10) || nmin;
2220      if (nightsSel < nmin) nightsSel = nmin;
2221      if (nightsSel > nmax) nightsSel = nmax;
2222      var website =
2223        (qs('input[name="website"]', reqHost) && qs('input[name="website"]', reqHost).value) || "";
2224      var errEl = qs("#bw-ho-err", reqHost);
2225      function setErr(t) {
2226        if (errEl) errEl.textContent = t || "";
2227      }
2228      setErr("");
2229      if (!gs.trim() || !gf.trim() || !ph.trim()) {
2230        setErr("Заполните фамилию, имя и телефон");
2231        return;
2232      }
2233      if (guestEmailRequired() && !isValidGuestEmail(em)) {
2234        setErr("Укажите корректный email");
2235        return;
2236      }
2237      var consent = qs("#bw-ho-pd-consent", reqHost);
2238      var terms = qs("#bw-ho-guest-terms", reqHost);
2239      if (!consent || !consent.checked || !terms || !terms.checked) {
2240        setErr("Примите соглашение модуля и дайте согласие на обработку персональных данных");
2241        return;
2242      }
2243      if (
2244        state.config &&
2245        state.config.captcha_enabled &&
2246        String(state.config.captcha_provider || "altcha").toLowerCase() !== "none" &&
2247        !state.captchaToken
2248      ) {
2249        setErr("Пройдите проверку капчи");
2250        return;
2251      }
2252      var body = {
2253        check_in: item.check_in,
2254        check_out: addDaysIso(item.check_in, nightsSel),
2255        adults: ad,
2256        children: ch,
2257        guest_surname: gs.trim(),
2258        guest_first_name: gf.trim(),
2259        guest_name: (gs.trim() + " " + gf.trim()).trim(),
2260        phone: ph.trim(),
2261        email: em.trim() || null,
2262        notes: no.trim() || null,
2263        captcha_token: state.captchaToken || null,
2264        website: website || null,
2265        hot_offer: true,
2266        pd_consent: true,
2267        guest_terms_accepted: true,
2268      };
2269      if (item.kind === "room" && item.room_id) body.room_id = item.room_id;
2270      else if (item.room_category_id) body.room_category_id = item.room_category_id;
2271      else {
2272        setErr("Не выбран номер");
2273        return;
2274      }
2275
2276      var btn = qs("#bw-ho-submit", reqHost);
2277      if (btn) btn.disabled = true;
2278      fetch(apiUrl("/api/v1/widget/requests", { organization_id: orgId, token: token }), {
2279        method: "POST",
2280        credentials: "omit",
2281        headers: { "Content-Type": "application/json" },
2282        body: JSON.stringify(body),
2283      })
2284        .then(function (r) {
2285          if (r.status === 429) throw new Error("Слишком много запросов или дубликат заявки");
2286          if (!r.ok)
2287            return r.json().then(function (j) {
2288              var d = j.detail;
2289              var msg = typeof d === "string" ? d : d && d[0] && d[0].msg ? d[0].msg : "Ошибка";
2290              throw new Error(msg);
2291            });
2292          return r.json();
2293        })
2294        .then(function () {
2295          closeRequestModal();
2296          setMsg("Заявка отправлена. Мы свяжемся с вами.", false);
2297        })
2298        .catch(function (e) {
2299          setErr(e.message || "Ошибка");
2300        })
2301        .finally(function () {
2302          syncSubmitWithPdConsent(
2303            qs("#bw-ho-submit", reqHost),
2304            qs("#bw-ho-pd-consent", reqHost),
2305            qs("#bw-ho-guest-terms", reqHost),
2306          );
2307        });
2308    }
2309
2310    function guestsLabel(item) {
2311      var cap = item.max_guests != null ? item.max_guests : item.bed_count;
2312      if (cap == null || cap === "" || isNaN(Number(cap)) || Number(cap) < 1) return "";
2313      var n = Number(cap);
2314      var x = Math.abs(n) % 100;
2315      var y = x % 10;
2316      if (n % 10 === 1 && n % 100 !== 11) return "до " + n + " гостя";
2317      if (y >= 2 && y <= 4 && !(x >= 12 && x <= 14)) return "до " + n + " гостей";
2318      return "до " + n + " гостей";
2319    }
2320
2321    function formatArea(area) {
2322      if (area == null || area === "" || isNaN(Number(area))) return "";
2323      var v = Math.round(Number(area) * 10) / 10;
2324      return String(v).replace(".", ",") + " м²";
2325    }
2326
2327    function buildPhotoOverlay(item) {
2328      var overlay = el("div", { class: "bw-hot-photo-overlay", tabindex: "0" }, []);
2329      overlay.appendChild(el("p", { class: "bw-hot-ov-title" }, [item.public_name || "Номер"]));
2330      var facts = el("div", { class: "bw-hot-ov-facts" }, []);
2331      var guests = guestsLabel(item);
2332      if (guests) facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, [guests]));
2333      var area = formatArea(item.area_sqm);
2334      if (area) facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, [area]));
2335      if (item.floor != null && item.floor !== "")
2336        facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, ["этаж " + item.floor]));
2337      if (facts.childNodes.length) overlay.appendChild(facts);
2338      var desc = String(item.description || "").trim();
2339      if (desc) overlay.appendChild(el("p", { class: "bw-hot-ov-desc" }, [desc]));
2340      var tags = item.amenity_highlights;
2341      if (tags && tags.length) {
2342        var wrap = el("div", { class: "bw-hot-ov-tags" }, []);
2343        tags.slice(0, 8).forEach(function (t) {
2344          wrap.appendChild(el("span", { class: "bw-hot-ov-tag" }, [String(t)]));
2345        });
2346        overlay.appendChild(wrap);
2347      }
2348      if (!facts.childNodes.length && !desc && !(tags && tags.length)) {
2349        overlay.appendChild(el("p", { class: "bw-hot-ov-desc" }, ["Подробности уточняйте у администратора"]));
2350      }
2351      return overlay;
2352    }
2353
2354    function renderCards() {
2355      listHost.innerHTML = "";
2356      if (!state.items.length) {
2357        listHost.appendChild(el("div", { class: "bw-hot-msg" }, ["Сейчас нет горящих предложений."]));
2358        return;
2359      }
2360      var trackClass = "bw-hot-track";
2361      if (state.layout === "vertical") trackClass += " bw-hot-track--vertical";
2362      else if (state.layout === "tiles") trackClass += " bw-hot-track--tiles";
2363      var track = el("div", { class: trackClass }, []);
2364      state.items.forEach(function (item) {
2365        var card = el("article", { class: "bw-hot-card" }, []);
2366        card.appendChild(el("div", { class: "bw-hot-badge" }, ["−" + item.discount_percent + "%"]));
2367        var photoAttrs = { class: "bw-hot-photo" };
2368        if (state.photoClickUrl) {
2369          photoAttrs.href = state.photoClickUrl;
2370        }
2371        var photo = el(state.photoClickUrl ? "a" : "div", photoAttrs, []);
2372        var media0 = (item.media_urls && item.media_urls[0]) || "";
2373        if (media0) {
2374          photo.appendChild(el("img", { src: widgetAssetUrl(media0), alt: item.public_name || "" }));
2375        }
2376        photo.appendChild(buildPhotoOverlay(item));
2377        if (!state.photoClickUrl) {
2378          photo.addEventListener("click", function (ev) {
2379            // На тач-устройствах без hover — показать/скрыть описание тапом по фото
2380            if (ev.target && ev.target.closest && ev.target.closest("a,button")) return;
2381            var open = photo.classList.contains("bw-hot-photo--open");
2382            var opened = root.querySelectorAll(".bw-hot-photo--open");
2383            for (var i = 0; i < opened.length; i++) opened[i].classList.remove("bw-hot-photo--open");
2384            if (!open) photo.classList.add("bw-hot-photo--open");
2385          });
2386        }
2387        card.appendChild(photo);
2388        var body = el("div", { class: "bw-hot-body" }, []);
2389        body.appendChild(el("h4", { class: "bw-hot-name" }, [item.public_name || "Номер"]));
2390        var guests = guestsLabel(item);
2391        if (guests) body.appendChild(el("p", { class: "bw-hot-meta" }, [guests]));
2392        body.appendChild(
2393          el("p", { class: "bw-hot-dates" }, [
2394            "Заезд " + formatIsoRu(item.check_in) + " · " + freeNightsLabel(item),
2395          ]),
2396        );
2397        body.appendChild(
2398          el("div", { class: "bw-hot-prices" }, [
2399            el("span", { class: "bw-hot-old" }, [formatMoney(item.old_price_cents)]),
2400            el("span", { class: "bw-hot-new" }, [
2401              formatMoney(item.price_cents),
2402              el("small", {}, [" · от " + nightsWord(item.nights_min || item.nights || 1)]),
2403            ]),
2404          ]),
2405        );
2406        var actions = el("div", { class: "bw-hot-actions" }, []);
2407        actions.appendChild(
2408          el(
2409            "button",
2410            {
2411              type: "button",
2412              class: "bw-hot-cta",
2413              onclick: function () {
2414                openRequestModal(item);
2415              },
2416            },
2417            ["Забронировать"],
2418          ),
2419        );
2420        var tel = telHref(state.contactPhone);
2421        if (tel) {
2422          actions.appendChild(
2423            el("a", { class: "bw-hot-cta bw-hot-cta--secondary", href: tel }, ["Позвонить"]),
2424          );
2425        }
2426        if (state.maxUrl) {
2427          actions.appendChild(
2428            el(
2429              "a",
2430              {
2431                class: "bw-hot-cta bw-hot-cta--max",
2432                href: state.maxUrl,
2433                target: "_blank",
2434                rel: "noopener noreferrer",
2435              },
2436              ["Написать в MAX"],
2437            ),
2438          );
2439        }
2440        body.appendChild(actions);
2441        card.appendChild(body);
2442        track.appendChild(card);
2443      });
2444      listHost.appendChild(track);
2445    }
2446
2447    setMsg("Загрузка…", false);
2448    Promise.all([
2449      fetch(apiUrl("/api/v1/widget/config", { organization_id: orgId, token: token }), { credentials: "omit" }).then(
2450        function (r) {
2451          if (!r.ok) throw new Error("config");
2452          return r.json();
2453        },
2454      ),
2455      fetch(apiUrl("/api/v1/widget/hot-offers", { organization_id: orgId, token: token }), { credentials: "omit" }).then(
2456        function (r) {
2457          if (!r.ok) throw new Error("hot-offers");
2458          return r.json();
2459        },
2460      ),
2461    ])
2462      .then(function (pair) {
2463        state.config = pair[0];
2464        var data = pair[1] || {};
2465        state.items = Array.isArray(data.items) ? data.items : [];
2466        state.contactPhone = String(
2467          data.contact_phone || (pair[0] && pair[0].contact_phone) || "",
2468        ).trim();
2469        state.maxUrl = String(data.max_url || (pair[0] && pair[0].max_url) || "").trim();
2470        state.photoClickUrl = String(data.photo_click_url || "").trim();
2471        if (!layoutOpt && data.default_layout) {
2472          var dl = String(data.default_layout || "").toLowerCase();
2473          if (dl === "vertical" || dl === "tiles" || dl === "horizontal") state.layout = dl;
2474        }
2475        setMsg("", false);
2476        renderCards();
2477      })
2478      .catch(function () {
2479        setMsg("Не удалось загрузить горящие предложения", true);
2480      });
2481  }
2482
2483  global.BookingWidget = { init: init };
2484})(typeof window !== "undefined" ? window : this);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.