PageSourceSearch

https://victoriaspb.ru/js/user.js

js victoriaspb.ru collected 2026-10-06 13:17:35 UTC 104,169 bytes, 2,817 lines download raw bytes

1var myMap, firstGeoObject; // Переменные для карты
2var mouseClick, hashChanged = "", tmp_timer; // Переменные для перехвата AJAX перехода
3var memMenu, page, flag, u_promo = "";
4let lazyScriptsLoaded = false;
5
6document.addEventListener("DOMContentLoaded", e => {
7
8  bindEvents();
9
10  OPTIONS.reload = false;
11
12  document.addEventListener('click', e => {
13
14    const link = e.target.closest('a[href^="#"]');
15    if(!link) return;
16
17    const id = link.getAttribute('href');
18
19    // пропускаем пустые якоря
20    if(id === '#' || id.length < 2) return;
21
22    const el = document.querySelector(id);
23    if(!el) return;
24
25    e.preventDefault();
26
27    el.scrollIntoView({
28      behavior: 'smooth',
29      block: 'start'
30    });
31
32  });
33  
34  if (window.matchMedia("(hover: none)").matches) {
35
36    document.addEventListener('click', function(e){
37
38      const el = e.target.closest('[data-tooltip]');
39      const opened = document.querySelector('.tooltip-open');
40
41      if(opened && !el){
42        opened.classList.remove('tooltip-open');
43        return;
44      }
45
46      if(el){
47        el.classList.toggle('tooltip-open');
48      }
49
50    });
51  }
52  
53  const mobileSearch = document.querySelector("#mobile-search");
54  mobileSearch.addEventListener("click", e => {
55    e.stopPropagation();
56    let data = `<div id="dialog-search"><input type="search" name="search_input" placeholder="Найти экскурсию"><button id="search">Поиск</button></div>`;
57    createFlyWindow(data);
58    setTimeout(() => {
59      const button_search = document.querySelector("#dialog-search #search");
60      if (button_search) {
61          button_search.addEventListener("click", e => {
62              const str = e.target.closest("#dialog-search").querySelector("input").value;
63              const bytes = new TextEncoder().encode(str);
64              const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
65              const hash = btoa(binary)
66                  .replace(/\+/g, '-')
67                  .replace(/\//g, '_')
68                  .replace(/=+$/, '');
69                  
70              window.location.href = "/tours/search?title=" + hash;
71          });
72      }
73    }, 500);
74  });
75  
76  const search = document.querySelector("#pc input[type=search]");
77  if (search) {
78      search.addEventListener("keydown", e => {
79          if (e.key === 'Enter') {
80              e.preventDefault(); // чтобы форма не отправилась стандартно
81              
82              const str = e.target.value;
83              const bytes = new TextEncoder().encode(str);
84
85              const binary = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
86              const hash = btoa(binary)
87                  .replace(/\+/g, '-')
88                  .replace(/\//g, '_')
89                  .replace(/=+$/, '');
90
91              window.location.href = "/tours/search?title=" + hash;
92          }
93      });
94  }
95
96  if (window.location.pathname != "/") {
97    if (window.innerWidth > 640) {
98      document.querySelector("section#article_page").style.paddingTop = "60px";
99    } else {
100      document.querySelector("section#article_page").style.paddingTop = "130px";
101    }
102  }
103
104  document.querySelector('.openDate')?.addEventListener('click', function() {
105    const input = document.querySelector('input[name="tdate"]');
106    input.focus();
107    
108    // Попытка эмулировать клик для открытия календаря
109    const event = new MouseEvent('click', {
110        bubbles: true,
111        cancelable: true,
112        view: window
113    });
114    input.dispatchEvent(event);
115  });
116  
117  if (window.matchMedia("(max-width: 640px)").matches) {
118
119    const mobile = document.getElementById("mobile-menu");
120    const mobile_buton = document.getElementById("mobile");
121
122    mobile_buton.addEventListener("click", (e) => {
123      // не даём summary закрывать меню
124      if (e.target.closest("summary")) return;
125      mobile.classList.toggle("open");
126      mobile_buton.classList.toggle("open");
127
128      const header = mobile.closest("header");
129
130      if (mobile.classList.contains("open")) {
131        if (window.scrollY == 0) {
132          OPTIONS.header["invert"] = true;
133          header.style.backgroundColor = "#f9f8f8";
134          header.style.marginTop = "24px";
135          header.style.padding = "0 12px";
136          header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', '#e95275'));
137          mobile.style.margin = "8px 0px 0px -12px";
138          header.querySelector("#pc").style.padding = "6px 0";
139          header.style.height = "auto";
140        } else {
141          OPTIONS.header["invert"] = false;
142          mobile.style.margin = "8px 0 0 0";
143          header.style.padding = "0";
144          header.style.height = "110px";
145        }
146
147        header.querySelector("a.menu-button").style.color = "white";
148      } else {
149        if (window.location.pathname == "/" && window.scrollY == 0) {
150          header.style.backgroundColor = "transparent";
151          header.style.marginTop = "24px";
152          header.style.padding = "8px 16px";
153          header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', '#FFF'));
154          header.querySelector("#pc").style.padding = "6px 12px";
155          document.querySelector("#mobile-search").style.filter = "brightness(2)";
156        } else {
157          header.style.background = "#f9f8f8";
158          header.style.marginTop = "28px";
159          header.style.padding = "0";
160          header.querySelector("#pc").style.padding = "6px 12px";
161          header.querySelector("#header-section .header-logo").style.height = "48px";
162          header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', '#e95275'));
163          header.style.height = "70px";
164          document.querySelector("#mobile-search").style.filter = "brightness(0)";
165        }
166
167      }
168    });
169  }
170
171  const dreit = document.querySelector("#dreit");
172  if (dreit) {
173    dreit.addEventListener("click", e => {
174      if (e.target.classList.contains("star")) {
175        let reit = parseInt(e.target.dataset.value);
176        for (let i = 1; i <= reit + 1; i++) {
177          dreit.querySelector(`[data-value="${i}"]`)?.setAttribute("class", "star");
178        }
179        for (let j = reit + 1; j <= 6; j++) {
180          dreit.querySelector(`[data-value="${j}"]`)?.setAttribute("class", "star half_star");
181        }
182        document.querySelector("input[name=reit]").value = reit;
183        dreit.previousElementSibling.querySelector("span").textContent = reit;
184      }
185    });
186  }
187
188  document.querySelector("#site_buy")?.addEventListener("click", e => {
189      e.preventDefault();
190      e.currentTarget.style.display = "none";
191      document.querySelector("div#office_buy").style.display = "block";
192  });
193
194  let sw = document.body.clientWidth;
195
196  const details = document.querySelectorAll("footer details, header details");
197  if (details) {
198      if (sw < 760) {
199          details.forEach(el => {
200              el.removeAttribute("open");
201          });
202      }
203      else {
204          details.forEach(el => {
205              el.setAttribute("open", true);
206          });
207      }
208  }
209
210  let feeds = document.querySelectorAll("a[data-feed]");
211  if (feeds) {
212      feeds.forEach(feed => {
213        feed.addEventListener("click", e => {
214            e.preventDefault();
215              let id = e.target.getAttribute("data-feed");
216
217              fd = new FormData();
218              fd.append("data_type", "feedback");
219              fd.append("id", id);
220
221              tmp = postData("../user/getAjaxData.php", fd, false, "GET").then(response => {
222                  try {
223                      let resp = JSON.parse(response);
224                      if (/^ok/.test(resp[0])) {
225                          createFlyWindow(resp.html);
226                      }
227                      else {
228                          console.log(response);
229                          alert("Что то пошло не так!");
230                      }
231                  }
232                  catch (err) {
233                      console.error("Ошибка загрузки страницы, фиды", err);
234                  }
235              });
236          });
237      });
238  }
239                      
240  // обработка ESC
241  document.addEventListener("keyup", e => {
242      if (e.key === "Escape") {
243          // скрываем блоки с плавностью
244          document.querySelectorAll(".flyTimeWindow_menu, .flyTimeWindow_article, .flyOrderWindow")
245          .forEach(el => el.classList.add("hidden"));
246
247          // скрываем и удаляем основные окна
248          document.querySelectorAll("div#view_road, div#bg-big-img, div.win-bg, .flyTimeWindow")
249          .forEach(el => {
250            el.classList.add("hidden");
251            el.classList.remove("visible");
252            el.remove();
253          });
254
255      }
256  });
257
258  // проверка payResponse
259  if (/payResponse\//.test(window.location.href)) {
260      const checkLink = document.querySelector("a#check_link");
261      if (checkLink && checkLink.getAttribute("href")) {
262          const check = checkLink.getAttribute("href");
263
264          const params = new URLSearchParams(window.location.search);
265          const orderId = params.get('orderId');
266        
267          // создаём билет
268          const fd1 = new FormData();
269          fd1.append("data", "create_check");
270          fd1.append("link", check);
271          fd1.append("orderId", orderId);
272        
273          postData("../user/saveData.php", fd1).then(response => {
274            if (!response.ok) {
275                console.log(response);
276                const html = `
277                  <div class="win-close" onclick="closeFlyWindow();"></div>
278                  <img src="../images/site-errror.jpg" />
279                  <h2 style="text-align: center">Создание билета</h2>
280                  <p>Не удалось создать билет, пожалуйста свяжитесь с нашим офисом:</p>
281                  <span style="display: block; text-align: center; font-size: 1.5em; color: #fd736c; margin: 0.5em;">+7 (812) 441-33-23</span>
282                  <button onclick="closeFlyWindow();">Хорошо</button>`;
283                  createFlyWindow(html);
284            } else {
285                const menu = document.querySelector("input#tour_sale_id").value;
286
287                // отправляем письмо
288                const fd2 = new FormData();
289                fd2.append("data", "send_mail");
290                fd2.append("id_tour", menu);
291
292                postData("../user/saveData.php", fd2).then(response2 => {
293                  if (!response2.ok) {
294                    const html = `
295                      <div class="win-close" onclick="closeFlyWindow();"></div>
296                      <img src="../images/site-errror.jpg" />
297                      <h2 style="text-align: center">Отправка уведомления</h2>
298                      <p>Ошибка: ${response2}</p>
299                      <p>Не удалось отправить сообщение с билетом на указанный Вами email, пожалуйста скачайте билет со ссылки на данной странице.<br>Телефон нашего офиса:</p>
300                      <span style="display: block; text-align: center; font-size: 1.5em; color: #fd736c; margin: 0.5em;">+7 (812) 441-33-23</span>
301                      <button onclick="closeFlyWindow();">Хорошо</button>`;
302                      createFlyWindow(html);
303                  }
304                });
305
306                checkLink.textContent = "Скачать билет";
307            }
308          });
309      }
310  }
311
312  if (/tour\//.test(window.location.href)) {
313
314    // ==== Галерея
315    new HeroGallery(".box-hero__body");
316    FeedBackSlider();
317    renderExcursionDate();
318
319    // ==== получаем промокод по заросу
320    const u_promo_cmd = document.querySelector("#u_promo_cmd");
321    u_promo_cmd?.addEventListener("click", e => {
322
323      const urlObj = new URL(window.location.href);
324      const tour = urlObj.pathname.split('/').pop(); // Получаем имя тура
325      const promo = document.querySelector("input[name=u_promo]");
326        
327      const fd = new FormData();
328      fd.append("data_type", "u_promo");
329      fd.append("tour", tour);
330      fd.append("promo", promo.value);
331
332      postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
333        document.querySelector("input[name=promo_sum]").value = data;
334        recalcSum();
335        
336        let pm = document.querySelector("#promo_message");
337        if (parseInt(data) != 0) {
338          pm.style.color = "#006605ff";
339          promo.style.backgroundColor = "#b5fcb8ff";
340          pm.textContent = "Промокод применён";
341        } else {
342          pm.style.color = "#db0000ff";
343          promo.style.backgroundColor = "#fcb5b5ff";
344          pm.textContent = "Не нашли такой промокод";
345        }
346      });      
347    });
348  
349    // переключение предоплаты
350    const prepay_radio = document.querySelectorAll(".options input[type=radio]");
351    if (prepay_radio) {
352      prepay_radio.forEach(radio => {
353        radio.addEventListener("click", e => recalcSum());
354      });
355    }
356
357    // показать карту
358    const cmdRoad = document.querySelector("#roadMap");
359    cmdRoad?.addEventListener("click", e => {
360      e.preventDefault();
361
362      const tour_id = e.target.closest("a").dataset.tour;
363      showRoadMap(tour_id);
364    });
365
366    const points = document.querySelectorAll(".spollers__item");
367    if (points.length) {
368      points.forEach(point => {
369        point.addEventListener("click", e => {
370          const clicked = e.currentTarget; // элемент, на котором висит слушатель
371          
372          // 1. Убираем show у ВСЕХ элементов коллекции
373          points.forEach(p => {
374            if (p !== clicked) p.classList.remove("show");
375          });
376          
377          // 2. Добавляем show только к кликнутому
378          if (!clicked.classList.contains("show")) {
379            clicked.classList.add("show");
380          } else {
381            clicked.classList.remove("show");
382          }
383        });
384      });
385    }
386  }
387
388  // изменение даты
389  const input = document.querySelector("input[name=d_start]");
390  input?.addEventListener("input", e => {
391    if (!input.value) return;
392
393    const [year, month, day] = input.value.split('-').map(Number);
394
395    // Проверка года
396    if (year < 1900 || year > 2100) return;
397
398    // Проверка нативной валидности
399    if (!input.validity.valid) return;
400
401    // Небольшая задержка — UX
402    setTimeout(() => {
403        window.location.href= `/tours/all?date=${input.value}`;
404    }, 300);
405  });
406  
407  if (getCookie("userCookie")) {
408      let cBlock = document.querySelector("div#cookieBlock");
409      cBlock.style.display = "none";
410  }
411
412  // bindGalery();
413  load_lazy_scripts();
414  window.onscroll();
415
416});
417
418document.addEventListener("click", event => {
419
420  if (OPTIONS.justOpenedWindow) return;
421
422  // если клик внутри указанных элементов — ничего не делаем
423  if (event.target.closest("#bg-big-img, .flyTimeWindow, .flyTimeWindow_menu, .flyOrderWindow, .window")) {
424      return;
425  }
426
427  // скрываем и удаляем основные окна
428  document.querySelectorAll("div#bg-big-img, .flyTimeWindow, div.win-bg, #view_road")
429    .forEach(el => {
430      el.classList.add("hidden"); // плавность обеспечивается CSS
431      el.classList.remove("visible");
432      el.remove();
433    });
434
435  // скрываем вспомогательные блоки
436  document.querySelectorAll(".flyTimeWindow_menu, .flyTimeWindow_article, #view_road, .window")
437    .forEach(el => {
438      el.classList.add("hidden");
439      el.classList.remove("visible");
440    });
441
442  if (!event.target.closest('[data-run-script="true"]')) {
443    event.stopPropagation();
444  }
445});
446
447/**
448 * Передача параметров меджу функциями
449 */
450const handler = {
451    get(target, prop) {
452        return target[prop];
453    },
454    set(target, prop, value) {
455        target[prop] = value;
456        return true;
457    }
458};
459
460const OPTIONS = new Proxy({
461  header: { "invert" : false }
462}, handler);
463
464window.OPTIONS = OPTIONS;
465
466window.onscroll = e => {
467
468  if (window.innerWidth <= 640) {
469    if (booking_block) {
470      const goto_booking = document.querySelector("#goto_booking").closest(".mobile-fixed");
471      if (OPTIONS.booking_block_visible) {
472        if (!isVisible(goto_booking)) {
473          goto_booking.style.display = "none";
474        } else {
475          goto_booking.style.display = "flex";
476        }
477      } else {
478        if (!isVisible(goto_booking)) {
479          goto_booking.style.display = "flex";
480        } else {
481          goto_booking.style.display = "none";
482        }
483      }
484    }
485  }
486
487  let header = document.querySelector("header");
488
489  if (!window.matchMedia("(max-width: 640px)").matches) {
490    if ((window.location.pathname == "/" && !/payRequest/.test(window.location.href)) && window.scrollY == 0) {
491      OPTIONS.header["invert"] = false;
492      document.getElementById("pc").classList.remove("gray");
493      header.style.background = "transparent";
494      header.style.marginTop = "56px auto";
495      header.style.borderBottom = "none";
496      header.querySelector("#header-section").style.margin = "0 auto";
497      header.style.padding = "8px 16px";
498      header.querySelector("#header-section .header-logo").style.height = "62px";
499      header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', "#FFF"));
500      header.querySelector("#mobile").classList.remove("gray");
501      header.querySelectorAll("#pc > li > a").forEach(el => {
502        if (!el.classList.contains("menu-button")) {
503          el.style.color = "white";
504        }
505      });
506      document.querySelector("#mobile-search").style.filter = "brightness(0)";
507    } else {
508      if (!OPTIONS.header["invert"]) {
509        OPTIONS.header["invert"] = true;
510        document.getElementById("pc").classList.add("gray");
511        header.style.background = "#f9f8f8";
512        header.style.marginTop = "34px";
513        header.style.padding = "6px 12px";
514        header.style.borderBottom = "solid 2px #E75375";
515        header.querySelector("#header-section .header-logo").style.height = "48px";
516        header.querySelector("#header-section").style.margin = "0 auto";
517        header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', '#e95275'));
518        header.querySelector("#mobile").classList.add("gray");
519        header.querySelectorAll("#pc > li > a").forEach(el => {
520          if (!el.classList.contains("menu-button")) {
521            el.style.color = "black";
522          }
523        });
524        document.querySelector("#mobile-search").style.filter = "brightness(2)";
525      }
526    }
527  } else {
528    if (window.location.pathname == "/" && window.scrollY == 0) {
529      header.querySelector('#mobile-menu').classList.remove("open");
530      header.querySelector('#mobile').classList.remove("open");
531      header.querySelectorAll("#pc > li > a").forEach(el => {
532        if (!el.classList.contains("menu-button")) {
533          el.parentElement.style.display = "none";
534        }
535      });
536
537      OPTIONS.header["invert"] = false;
538      header.style.backgroundColor = "transparent";
539      header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', "#FFF"));
540      header.querySelector('#header-section .header-logo').style.height = "62px";
541      header.querySelector('#mobile-menu').style.margin = "108px 0 0 -32px";
542      header.style.marginTop = "32px";
543      header.style.padding = "0 16px";
544      header.style.height = "110px";
545      document.querySelector("#mobile-search").style.filter = "brightness(2)";
546    } else {
547      if (!OPTIONS.header["invert"]) {
548        header.querySelector('#mobile-menu').classList.remove("open");
549        header.querySelector('#mobile').classList.remove("open");
550        header.querySelectorAll("#pc > li > a").forEach(el => {
551          if (!el.classList.contains("menu-button")) {
552            el.parentElement.style.display = "none";
553          }
554        });
555
556        OPTIONS.header["invert"] = true;
557        header.style.backgroundColor = "white";
558        header.querySelectorAll('#header-section .header-logo path').forEach(p => p.setAttribute('fill', '#e95275'));
559        header.querySelector('#header-section .header-logo').style.height = "48px";
560        header.querySelector('#mobile-menu').style.margin = "8px 0 0 12px";
561        header.style.marginTop = "28px";
562        header.style.padding = "0 12px";
563        header.style.height = "68px";
564        document.querySelector("#mobile-search").style.filter = "brightness(0)";
565      }
566    }
567  }
568}
569
570function initYMap()
571{
572    if (document.querySelector("div#ymap")) {
573        
574        myMap = new ymaps.Map("ymap", {
575            center: [59.937296, 30.317305],
576            zoom: 16,
577            controls: ['zoomControl']
578        });
579
580        document.querySelector('ymaps[class*="ground-pane"]').style.filter = "grayscale(90%)";
581        
582        const myPlacemark = new ymaps.Placemark([59.937296, 30.317305],
583            { 
584                iconContent: 'ул. Большая Морская, дом 3-5',
585                balloonContent: "<div class='bcontent'><p style='font-weight: bold; color: #dc117a; font-size: 16px;'>Офис туристической<br />фирмы &laquo;Виктория&raquo;</p><p>Санкт-Петербург<br />ул. Большая Морская, дом 3-5, офис 427<br />БЦ «Лидваль»<br />тел. 8 800 234 12 16</p></div>"
586            }, {
587                preset: 'islands#violetStretchyIcon'
588            });
589    
590        myMap.geoObjects.add(myPlacemark);
591    }
592    else if (document.querySelector("div.map_place")) {
593
594        try {
595            console.log("map_place JSON:", document.querySelector("div.map_place").getAttribute("data_addr"));
596            let data = JSON.parse(document.querySelector("div.map_place").getAttribute("data_addr"));
597
598            myMap = new ymaps.Map("tour_path_scheme", {
599                center: [59.938793, 30.314430],
600                zoom: 16,
601                controls: ['zoomControl']
602            });
603            myMap.behaviors.disable('scrollZoom');
604            
605            /* Начальный адрес метки */
606            var address = 'Россия, Санкт-Петербург, ' + data[1];
607            
608            ymaps.geocode(address).then(function (res) {
609                var coord = res.geoObjects.get(0).geometry.getCoordinates();
610            
611                var myPlacemark = new ymaps.Placemark(coord, {
612                    balloonContent: data[1],
613                    iconContent: data[0]
614                }
615                    , {
616                        preset: "islands#darkBlueStretchyIcon",
617                        // Отключаем кнопку закрытия балуна.
618                        balloonCloseButton: false,
619                        // Балун будем открывать и закрывать кликом по иконке метки.
620                        hideIconOnBalloonOpen: false
621                    });
622                
623                myMap.geoObjects.add(myPlacemark);
624                myMap.setCenter(coord, 15);
625            });
626        }
627        catch (err) {
628            console.error("Ошибка загрузки данных карты", err);
629        }
630    }
631}
632
633//Выбираем данные
634function getData(data_type, menu, id) {
635  document.body.style.cursor = "progress";
636
637  clearInterval(mouseClick); // снимаем отслеживание перехода по хэш
638
639  // формируем данные
640  let FD = new FormData();
641  FD.append("data_type", data_type);
642  FD.append("menu", menu);
643  FD.append("id", id);
644
645  postData("../user/getAjaxData.php", FD, false, "GET").then(response => {
646    if (data_type === "timetable" || data_type === "tours") {
647      document.querySelector("section#article_page").innerHTML = response;
648
649    } else if (data_type === "tourfiltr" || data_type === "catalog") {
650      document.querySelector("div#tour_cards").innerHTML = response;
651      window.scrollTo({ top: 290, behavior: "smooth" });
652
653    } else if (data_type === "feedback") {
654      document.querySelector("section#article_page").innerHTML = response;
655      if (/^r_.*/.test(id)) {
656        window.scrollTo({ top: 250, behavior: "smooth" });
657        const respInput = document.querySelector("#user_response");
658        if (respInput) respInput.focus();
659      }
660
661    } else if (data_type === "ufeedback") {
662      document.querySelector("section#article_page").innerHTML = response;
663
664    } else if (data_type === "faq") {
665      document.querySelector("div.blleft").innerHTML = response;
666    }
667
668    // запрет на Enter в input
669    document.querySelectorAll("input").forEach(inp => {
670      inp.addEventListener("keydown", e => {
671        if (e.keyCode === 13) e.preventDefault();
672      });
673    });
674
675    document.body.style.cursor = "default";
676  });
677}
678// Проверяем что текущая дата больге чем в аргементе
679function checkDate(targetDate, targetTime, closeTime) {
680
681  // разбираем дату
682  const [year, month, day] = targetDate.split('-');
683  const [hours, minutes] = targetTime.split(':');
684
685  // создаём объект даты экскурсии
686  const tourDateTime = new Date(
687      year,
688      month - 1, // JS месяцы с нуля
689      day,
690      hours,
691      minutes
692  );
693
694  // текущее время + буфер закрытия
695  const now = new Date();
696  const closeBorder = new Date(
697      tourDateTime.getTime() - closeTime * 60 * 1000
698  );
699
700  // проверка
701  return now >= closeBorder;
702}
703//Запрос экскурсий по часам на выбранный день на страницу расписания
704function getTourHours(date, printType, obj) {
705  document.body.style.cursor = "progress";
706
707  const ulSute = document.querySelector("div#ul_sute ul");
708  if (ulSute) {
709    ulSute.innerHTML = "<p style='text-align: center; padding: 150px 0; font-size: 17pt;'><img id='aj' src='../images/ajax-loader-big.gif' /></p>";
710  }
711
712  // переключаем активный день
713  document.querySelectorAll("div.myCalendar span.active").forEach(el => el.classList.remove("pick_day"));
714  obj.classList.add("pick_day");
715
716  setTimeout(() => {
717    const fd = new FormData();
718    fd.append("data_type", "get_tour_hours");
719    fd.append("menu", date);
720    fd.append("id", printType);
721
722    postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
723      if (!data || data.length <= 10) {
724        document.body.style.cursor = "auto";
725        return;
726      }
727
728      if (printType === 0) {
729        document.querySelectorAll("div.flyTimeWindow").forEach(el => el.remove());
730        
731        if (!document.querySelector("div.flyTimeWindow")) {
732          // создать блок
733          const timeWindow = document.createElement("div");
734          timeWindow.classList.add("flyTimeWindow");
735          timeWindow.innerHTML = data;
736          document.body.appendChild(timeWindow);
737
738          const linkCount = timeWindow.querySelectorAll('a').length;
739          const formatted = date.replace(/^(\d{4})-(\d{2})-(\d{2})$/, '$3.$2.$1');
740          // Добавляем заголовок
741          const title = `<h3><a href="/tours/all?date=${date}">Экскурсий: ${linkCount}</a> [${formatted}]</h3>`;
742          timeWindow.insertAdjacentHTML("afterbegin", title);
743
744          const rect = obj.getBoundingClientRect();
745          const objWidth = obj.offsetWidth;
746
747          timeWindow.classList.add("visible"); // плавность через CSS
748          const twWidth = 800;
749          
750          timeWindow.style.width = window.outerWidth > 640 ? twWidth + "px" : "100%";
751
752          timeWindow.style.left = window.outerWidth > 640 ? rect.left + 5 - (twWidth + rect.width) / 2 + "px" : 0;
753          timeWindow.style.top = window.outerWidth > 640 ? (rect.top + window.scrollY + 74) + "px" : (rect.top + window.scrollY + 44) + "px";
754
755          
756          document.body.style.cursor = "auto";
757        } 
758      } else {
759        const blleft = document.querySelector("div.blleft_inline");
760        if (blleft) blleft.innerHTML = data;
761        let count = document.querySelectorAll("div.blleft_inline #ul_sute li");
762        let title = document.querySelector("div.blleft_inline h3.time_out_title strong span");
763        if (title) {
764          title.insertAdjacentText("beforeend", ` [${[...count].length}]`);
765        } else {
766          let title = document.querySelector("div.blleft_inline h3.time_out_title strong");
767          title.insertAdjacentHTML("beforeend", `<span> [${[...count].length}]</span>`);
768        }
769        document.body.style.cursor = "auto";
770      }
771    });
772  }, 500);
773}
774// Потавить дату в поле
775function setInputDate(ndate, fs, obj) {
776  const input = document.querySelector("input[name=tdate]");
777  input.dataset.value = ndate;
778
779  const date = new Date(ndate);
780
781  // Форматируем день и месяц
782  const dayMonth = date.toLocaleDateString('ru-RU', {
783      day: 'numeric',
784      month: 'long'
785  });
786  
787  // Форматируем день недели
788  const weekday = date.toLocaleDateString('ru-RU', {
789      weekday: 'short'
790  });
791  
792  // Обрезаем точку у сокращения дня недели (Пт. -> Пт)
793  const weekdayShort = weekday.replace('.', '');
794  input.value =  `${dayMonth} (${weekdayShort})`;
795  
796  obj.closest(".flyTimeWindow").remove();
797}
798// Всплывающий календарь при выборе даты тура на странице тура и оплаты
799// id - id записи из таблицы расписания
800function showFlyCalendar(id, obj, udate, filter = false, foreng = 0) {
801  document.body.style.cursor = "progress";
802
803  // проверим, что пришло название поля
804  if (Object.prototype.toString.call(udate) !== '[object Date]') {
805    udate = document.querySelector(`input[name="${udate}"]`)?.value;
806  } 
807  
808  let picketDate = new Date(udate);
809
810  udate = picketDate.getFullYear() + '-' +
811        String(picketDate.getMonth() + 1).padStart(2, '0') + '-' +
812        String(picketDate.getDate()).padStart(2, '0');
813  
814  const fd = new FormData();
815  fd.append("data_type", "tourCalendar"); 
816  fd.append("id", id);
817  fd.append("udate", udate);
818  fd.append("filter", filter);
819  fd.append("foreng", foreng);
820
821  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
822    if (!document.querySelector("div.flyTimeWindow")) {
823      const timeWindow = document.createElement("div");
824      timeWindow.classList.add("flyTimeWindow");
825      timeWindow.innerHTML = data;
826      document.body.appendChild(timeWindow);
827
828      const rect = obj.getBoundingClientRect();
829      const objWidth = obj.offsetWidth;
830      const mqMobile = window.innerWidth;
831
832      if (mqMobile > 640) {
833        timeWindow.style.left = (rect.left + window.scrollX + objWidth / 2 - 200) + "px";
834        timeWindow.style.top = (rect.top + window.scrollY + obj.offsetHeight + 8 + 30) + "px";
835      } else {
836        timeWindow.style.left = "1rem";
837        timeWindow.style.top = (rect.top + window.scrollY + obj.offsetHeight + 6) + "px";
838      }
839      
840      timeWindow.style.width = "340px";
841      timeWindow.classList.add("visible");
842      document.body.style.cursor = "auto";
843    } else {
844      document.querySelectorAll("div.flyTimeWindow").forEach(el => el.remove());
845      document.body.style.cursor = "auto";
846    }
847  });
848}
849
850// подставляем ближайшую дату на страницу тура. Рендер происходит из кэша, даты не соответствуют
851// определить на сервере ближайшую дату в выдать массив 
852// алгоритм примерно такйо же как и клик при изменении даты
853function renderExcursionDate() {
854
855  const tour_id = document.querySelector("input[name=id_tour]")?.value;
856
857  if (!tour_id) {
858    return;
859  }
860
861  // Запросить блок цен
862  document.body.style.cursor = "progress";
863
864  const fd = new FormData();
865  fd.append("tour_id", tour_id);
866  fd.append("data_type", "tourHours");
867
868  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
869
870    let ourStarPointTime = [...document.querySelectorAll("#thisStartPointUp, #thisStartPointDown")];
871    let crossStarPointTime = [...document.querySelectorAll("#crossStartPointUp, #crossStartPointDown")];
872
873    try {
874      let times = JSON.parse(data);
875      if (times.hours.our.length != 0) {
876        ourStarPointTime.forEach(el => {
877          el.textContent = "";
878          let selected = " selected_date";
879          let disabled = "";
880          times.hours.our.forEach(time => {
881            if (checkDate(time["date_tour"], time["time_tour"], el.dataset.closetime)) {
882              disabled = " disabled_date";
883            } else {
884              disabled = "";
885            }
886            el.insertAdjacentHTML('beforeend', `<button type="button" class="application__time time-slot${selected}${disabled}" data-timeid="${time["id"]}" data-run-script="true" data-script="%7B%22script%22%3A%22setDate%22%2C%22param%22%3A%7B%22id%22%3A${time["id"]}%2C%22date%22%3A%22${time["date_tour"] + " " + time["time_tour"]}%22%2C%22foreng%22%3A0%7D%7D">${time["time_tour"]}</button>`);
887            selected = "";
888          });
889        });
890      }
891
892      // ==== Заменить блок цен, т.к. следим за датами акций
893      const cost_block = document.querySelector("#cost-block");
894      cost_block.innerHTML = times.cost_block;
895
896      const top_cost_block = document.querySelector("#top_cost_block");
897      top_cost_block.innerHTML = times.top_block;
898
899      // Показываем кроссы, если они есть
900      if (times.hours.cross.length != 0) {
901        crossStarPointTime.forEach(el => {
902          el.textContent = "";
903          let disabled = "";
904          times.hours.cross.forEach(time => {
905            if (checkDate(time["date_tour"], time["time_tour"], el.dataset.closetime)) {
906              disabled = " disabled_date";
907            } else {
908              disabled = "";
909            }
910            el.insertAdjacentHTML('beforeend', `<button type="button" class="application__time time-slot${disabled}" data-timeid="${time["id"]}" data-run-script="true" data-script="%7B%22script%22%3A%22setDate%22%2C%22param%22%3A%7B%22id%22%3A${time["id"]}%2C%22date%22%3A%22${time["date_tour"] + " " + time["time_tour"]}%22%2C%22foreng%22%3A1%7D%7D">${time["time_tour"]}</button>`);
911          });
912        });
913      } else {
914        crossStarPointTime.forEach(el => {
915          el.innerHTML = "";
916        });
917      }
918
919      // Теперь надо кликнуть по первому времени для выбора
920      let time_cmd = document.querySelector("#thisStartPointDown button.application__time:not(.disabled_date)");  
921      if (!time_cmd) {
922        time_cmd = document.querySelector("#crossStartPointDown button.application__time:not(.disabled_date)");
923        // А вот теперь у нас нет времени по кроссам
924        if (!time_cmd) {
925            time_cmd = document.querySelector("#crossStartPointDown button.application__time");
926        }
927      }
928
929      if (time_cmd) {
930          const event = new MouseEvent('click', {
931              bubbles: true,
932              cancelable: true,
933              view: window
934          });
935          time_cmd.dispatchEvent(event);
936      }
937
938      recalcSum();
939      
940    }
941    catch (err) {
942      console.error(err);
943    }
944    document.body.style.cursor = "auto";
945  });
946}
947function renderTimeButtons(container, times, foreng) {
948
949    container.textContent = "";
950
951    let selected = " selected_date";
952
953    times.forEach(time => {
954
955        const disabled = checkDate(
956            time.date_tour,
957            time.time_tour,
958            container.dataset.closetime
959        ) ? " disabled_date" : "";
960
961        container.insertAdjacentHTML("beforeend", `
962            <button
963                type="button"
964                class="application__time time-slot${selected}${disabled}"
965                data-timeid="${time.id}"
966                data-run-script="true"
967                data-script="%7B%22script%22%3A%22setDate%22%2C%22param%22%3A%7B%22foreng%22%3A${foreng}%2C%22id%22%3A${time.id}%2C%22date%22%3A%22${time.date_tour} ${time.time_tour}%22%7D%7D">
968                ${time.time_tour}
969            </button>
970        `);
971
972        // selected только для первой доступной
973        if (!disabled) {
974            selected = "";
975        }
976    });
977}
978//Подставляем дату в поля страницы экскурсий и покупки билета
979function displayDate(obj) {
980  // У нас есть id выбранного дня, но с турами по всему времени
981  // Все данные находятся в атрибуте date_data
982  const req_array = obj.getAttribute("date_data").split(";");
983
984  document.querySelector("#nearest_date").textContent = req_array[2];
985  document.querySelector("#nearest_date_buyblock").textContent = req_array[2];
986  document.querySelector("input[name=id_time]").value = req_array[0];
987  document.querySelector("input[name=sp_time]").value = req_array[5] + " " + req_array[1];
988
989  const flyWin = document.querySelector("div.flyTimeWindow"); 
990  if (flyWin) flyWin.remove();
991
992  // Запросить блок цен
993  document.body.style.cursor = "progress";
994
995  const fd = new FormData();
996  fd.append("data_type", "tourHours");
997  fd.append("tour_id", req_array[4]);
998  fd.append("date", req_array[5]); 
999
1000  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
1001
1002    let ourStarPointTime = [...document.querySelectorAll("#thisStartPointUp, #thisStartPointDown")];
1003    let crossStarPointTime = [...document.querySelectorAll("#crossStartPointUp, #crossStartPointDown")];
1004
1005    try {
1006      let times = JSON.parse(data);
1007      if (times.hours.our.length != 0) {
1008        ourStarPointTime.forEach(el => {
1009          el.textContent = "";
1010          let selected = " selected_date";
1011          let disabled = "";
1012          times.hours.our.forEach(time => {
1013            if (checkDate(time["date_tour"], time["time_tour"], el.dataset.closetime)) {
1014              disabled = " disabled_date";
1015            } else {
1016              disabled = "";
1017            }
1018            el.insertAdjacentHTML('beforeend', `<button type="button" class="application__time time-slot${selected}${disabled}" data-timeid="${time["id"]}" data-run-script="true" data-script="%7B%22script%22%3A%22setDate%22%2C%22param%22%3A%7B%22id%22%3A${time["id"]}%2C%22date%22%3A%22${time["date_tour"] + " " + time["time_tour"]}%22%2C%22foreng%22%3A0%7D%7D">${time["time_tour"]}</button>`);
1019            selected = "";
1020          });
1021        });
1022      }
1023
1024      // ==== Заменить блок цен, т.к. следим за датами акций
1025      const cost_block = document.querySelector("#cost-block");
1026      cost_block.innerHTML = times.cost_block;
1027
1028      const top_cost_block = document.querySelector("#top_cost_block");
1029      top_cost_block.innerHTML = times.top_block;
1030
1031      // Показываем кроссы, если они есть
1032      if (times.hours.cross.length != 0) {
1033        crossStarPointTime.forEach(el => {
1034
1035          let parentBlock = el.closest(".box-booking__item");
1036          if (!parentBlock) {
1037            parentBlock = el.closest(".application__block");
1038          }
1039          parentBlock.classList.remove("hidden");
1040
1041          el.textContent = "";
1042          let disabled = "";
1043          times.hours.cross.forEach(time => {
1044            if (checkDate(time["date_tour"], time["time_tour"], el.dataset.closetime)) {
1045              disabled = " disabled_date";
1046            } else {
1047              disabled = "";
1048            }
1049            el.insertAdjacentHTML('beforeend', `<button type="button" class="application__time time-slot${disabled}" data-timeid="${time["id"]}" data-run-script="true" data-script="%7B%22script%22%3A%22setDate%22%2C%22param%22%3A%7B%22id%22%3A${time["id"]}%2C%22date%22%3A%22${time["date_tour"] + " " + time["time_tour"]}%22%2C%22foreng%22%3A1%7D%7D">${time["time_tour"]}</button>`);
1050          });
1051        });
1052      } else {
1053        crossStarPointTime.forEach(el => {
1054
1055          let parentBlock = el.closest(".box-booking__item");
1056          if (!parentBlock) {
1057            parentBlock = el.closest(".application__block");
1058          }
1059          parentBlock.classList.add("hidden");
1060
1061          el.innerHTML = "";
1062        });
1063      }
1064
1065      // Теперь надо кликнуть по первому времени для выбора
1066      let time_cmd = document.querySelector("#thisStartPointDown button.application__time:not(.disabled_date)");  
1067      if (!time_cmd) {
1068        time_cmd = document.querySelector("#crossStartPointDown button.application__time:not(.disabled_date)");
1069      }
1070
1071      if (time_cmd) {
1072          const event = new MouseEvent('click', {
1073              bubbles: true,
1074              cancelable: true,
1075              view: window
1076          });
1077          time_cmd.dispatchEvent(event);
1078      }
1079
1080      recalcSum();
1081      
1082    }
1083    catch (err) {
1084      console.error(err);
1085    }
1086    document.body.style.cursor = "auto";
1087  });
1088}
1089//Выбираем время тура
1090function showFlyTime(obj) {
1091  const id = document.querySelector("input[name=id_time]").value;
1092
1093  const fd = new FormData();
1094  fd.append("data_type", "tourTimes");
1095  fd.append("id", id);
1096
1097  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
1098    if (!document.querySelector("div.flyTimeWindow")) {
1099      let width = 150;
1100      if (/neva/.test(data)) width = 450;
1101
1102      // создать блок
1103      const timeWindow = document.createElement("div");
1104      timeWindow.classList.add("flyTimeWindow");
1105      timeWindow.innerHTML = data;
1106      document.body.appendChild(timeWindow);
1107
1108      const rect = obj.getBoundingClientRect();
1109      const objWidth = obj.offsetWidth;
1110      const objHeight = obj.offsetHeight;
1111
1112      timeWindow.style.width = width + "px";
1113
1114      // центрируем относительно элемента
1115      const left = rect.left + window.scrollX + objWidth / 2 - width / 2 - 30;
1116      const top = rect.top + window.scrollY + objHeight;
1117
1118      timeWindow.style.left = left + "px";
1119      timeWindow.style.top = top  - 8 + "px";
1120
1121      // плавное появление через CSS‑transition
1122      timeWindow.classList.add("visible");
1123
1124      document.body.style.cursor = "auto";
1125    } else {
1126      document.querySelectorAll("div.flyTimeWindow").forEach(el => el.remove());
1127      document.body.style.cursor = "auto";
1128    }
1129  });
1130}
1131function displayTime(obj) {
1132  // Установить выбранное время
1133  // document.querySelector("div#picked_time").textContent = obj.textContent; // если нужно отобразить текст
1134  document.querySelector("input[name=id_time]").value = obj.getAttribute("date_id");
1135
1136  const flyWin = document.querySelector("div.flyTimeWindow");
1137  if (flyWin) flyWin.remove();
1138
1139  const menu = window.location.href.match(/\/tour\/([\w\d-,]*)#?/);
1140
1141  // Запросить блок цен
1142  document.body.style.cursor = "progress";
1143
1144  const fd = new FormData();
1145  fd.append("data_type", "tour");
1146  fd.append("menu", menu[1]);
1147  fd.append("id", obj.getAttribute("date_id"));
1148
1149  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
1150    const viewTour = document.querySelector(".view_tour_right");
1151    if (viewTour) viewTour.innerHTML = data;
1152
1153    // Обновить карту
1154    const map_block = document.querySelector("div.map_place");
1155    if (map_block) {
1156      map_block.innerHTML = "";
1157      const map_data = document.querySelector("div#pier");
1158      if (map_data) {
1159        map_block.setAttribute("data_addr", map_data.getAttribute("data_addr"));
1160        initYMap();
1161      }
1162    }
1163  });
1164}
1165function mainSearch(obj) {
1166  if (obj.value.length < 3) return false;
1167
1168  document.body.style.cursor = "progress";
1169
1170  const fd = new FormData();
1171  fd.append("data_type", "get_search");
1172  fd.append("menu", obj.value);
1173
1174  postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
1175    // если данных мало — выходим
1176    if (!data || data.length < 30) {
1177      document.body.style.cursor = "auto";
1178      return false;
1179    }
1180
1181    let flyWin = document.querySelector("div.flyTimeWindow");
1182
1183    if (!flyWin) {
1184      // создать блок
1185      flyWin = document.createElement("div");
1186      flyWin.classList.add("flyTimeWindow");
1187      flyWin.innerHTML = data;
1188      document.body.appendChild(flyWin);
1189
1190      const rect = obj.getBoundingClientRect();
1191      flyWin.style.width = "520px";
1192      flyWin.style.left = (rect.left - 260) + "px";
1193      flyWin.style.top = "478px";
1194
1195      // плавное появление через CSS
1196      flyWin.classList.add("visible");
1197
1198      document.body.style.cursor = "auto";
1199    } else {
1200      flyWin.innerHTML = data;
1201      document.body.style.cursor = "auto";
1202    }
1203  });
1204}
1205function getToursInterval()
1206{
1207    var b_date = document.querySelector("input[name=d_start]").value;
1208    var e_date = document.querySelector("input[name=d_end]").value;
1209    
1210    if (typeof b_date == "undefined" || typeof e_date == "undefined")
1211    {
1212        alert("Пожалуйста выберите интервал дат в полях и повторите запрос!");
1213    }else{
1214        var link = "/tours/interval_"+b_date+"_"+e_date;
1215        window.location.href = link;        
1216    }
1217    return false;
1218}
1219function mainSearchDate()
1220{
1221    var b_date = document.querySelector(":input[name=b_date]").value;
1222
1223    if (b_date.length == 0)
1224    {
1225        alert("Пожалуйста выберите дату в поле и повторите запрос!");
1226    }else{
1227        var link = "/tours/date_"+b_date;
1228        window.location.href = link;        
1229    }
1230}
1231// function flowTourHours(faa) {
1232//   const winEl = document.querySelector("div.blleft_inline");
1233//   const listEl = document.querySelector("ul.absolute");
1234
1235//   if (!winEl || !listEl) return;
1236
1237//   // высота окна и списка
1238//   const winHeight = winEl.offsetHeight - 148;
1239//   const listHeight = listEl.scrollHeight;
1240
1241//   if (listHeight <= winHeight) return;
1242
1243//   // текущий marginTop
1244//   let posList = parseInt(getComputedStyle(listEl).marginTop) || 0;
1245
1246//   if (listHeight - (winHeight - posList) > 0) {
1247//     if (Math.abs(posList) + winHeight < listHeight - winHeight) {
1248//       posList = posList - winHeight;
1249//       listEl.style.marginTop = posList + "px";
1250//     } else {
1251//       posList = listHeight - winHeight;
1252//       listEl.style.marginTop = "-" + posList + "px";
1253//     }
1254//   } else {
1255//     listEl.style.marginTop = "0px";
1256//   }
1257// }
1258
1259function recalcSum() {
1260
1261  let sum = 0, full_cost = 0, count = 0, sum_ppay = 0;
1262 
1263  const form = document.querySelector("form[name=ticket_count]"); 
1264  const tickets = form.querySelector("input[name=check_types]").value.split(",");
1265  const ppay = parseInt(form.querySelector("input[name=prepay_sum]")?.value) || 0;
1266  const payment_type = form.querySelector("input[name=fullPay]:checked")?.value || 1;
1267  const pay_sum = form.querySelector("input[name=pay_sum]");
1268
1269  tickets.forEach(ticket => {
1270    const cur_t_count = parseInt(form.querySelector(`input[name="tickets[${ticket}]"]`).value) || 0;
1271    const cur_t_cost = parseInt(form.querySelector(`input[name="cost[${ticket}]"]`).value) || 0;
1272    const ful_t_cost = parseInt(form.querySelector(`input[name="fcost[${ticket}]"]`).value) || 0;
1273
1274    full_cost += cur_t_count * ful_t_cost;
1275    sum += cur_t_count * cur_t_cost;
1276    sum_ppay += ppay * cur_t_count;
1277    count += cur_t_count;
1278  });
1279
1280  // Промокод
1281  let promo_sum = document.querySelector("input[name=promo_sum]").value;
1282  promo_sum *= count;
1283
1284  let promo_span = document.querySelector("#promo_sum span");
1285  if (promo_span) promo_span.textContent = promo_sum;
1286
1287  // Количество билетов
1288  let t_count = document.querySelector("#t_count");
1289  if (t_count) t_count.textContent = count; 
1290
1291  // Полная цена билетов
1292  let f_cost = document.querySelector("#full_cost span");
1293  if (f_cost) f_cost.textContent = full_cost;   
1294
1295  // скидка %
1296  let sale = document.querySelector("#sale");
1297  sale.textContent = parseInt(100 - (sum / full_cost * 100)) + "%" || 0 + "%";  
1298  // скидка деньгов
1299  let sale_sum = document.querySelector("#sale_sum span");
1300  sale_sum.textContent = parseInt(full_cost - sum);  
1301  // итого
1302  if (sum == 0) {
1303    // Не выбрали билеты, промокод не считаем
1304    promo_sum = 0;
1305  }
1306  let itog = document.querySelector("#itog span");
1307  itog.textContent = parseInt(sum - promo_sum); 
1308  // итого, предоплата
1309  let prepay = document.querySelector("#prepay span");
1310  // Доплата
1311  let doplata = document.querySelector("#doplata span"); 
1312
1313  if (payment_type != 1) {
1314    prepay.closest(".bottom-booking__box").style.display = "flex";
1315    doplata.closest(".bottom-booking__box").style.display = "flex";
1316    prepay.textContent = parseInt(sum_ppay);
1317    doplata.textContent = parseInt(sum - promo_sum - sum_ppay);
1318    pay_sum.value = parseInt(sum_ppay);
1319  } else {
1320    // Предоплата отключена
1321    prepay.closest(".bottom-booking__box").style.display = "none";
1322    doplata.closest(".bottom-booking__box").style.display = "none";
1323    prepay.textContent = parseInt(sum - promo_sum);
1324    pay_sum.value = parseInt(sum - promo_sum);
1325    
1326  }
1327
1328  return count;
1329  
1330}
1331function showRoadMap(id_tour) {
1332    
1333    var myMap = null; // объявляем в области видимости функции
1334
1335    function initRoadMap(obj) {
1336        // Проверка контейнера
1337        var container = document.getElementById("view_road");
1338        if (!container) {
1339            console.error('Контейнер #view_road не найден');
1340            return;
1341        }
1342
1343        // Инициализация карты
1344        myMap = new ymaps.Map("view_road", {
1345            center: obj.data.options.dispatch_points,
1346            zoom: 14,
1347            controls: ['zoomControl']
1348        });
1349
1350        // Добавляем все точки на карту
1351        ymapAddMarkers(obj);
1352        
1353        // Автоматически масштабируем карту под все метки
1354        fitMapToMarkers();
1355    }
1356
1357    // Добавление меток на карту — стандартные иконки
1358    function ymapAddMarkers(obj) {
1359        
1360        // 1. Старт — красная метка
1361        var startCoords = obj.data.options.dispatch_points;
1362        if (isValidCoords(startCoords)) {
1363            var startMarker = new ymaps.Placemark(
1364                startCoords,
1365                {
1366                    balloonContentHeader: "Старт",
1367                    balloonContentBody: obj.data.options.dp_name,
1368                    hintContent: "Начало экскурсии: " + obj.data.options.dp_name
1369                },
1370                {
1371                    preset: 'islands#redIcon'
1372                }
1373            );
1374            myMap.geoObjects.add(startMarker);
1375        }
1376
1377        // 2. Промежуточные точки — синие метки (стандартные)
1378        obj.data.points.forEach(function(item, index) {
1379            if (isValidCoords(item.point)) {
1380                var marker = new ymaps.Placemark(
1381                    item.point,
1382                    {
1383                        balloonContentHeader: "Остановка " + (index + 1),
1384                        balloonContentBody: item.title,
1385                        hintContent: item.title
1386                    },
1387                    {
1388                        preset: 'islands#blueIcon'
1389                    }
1390                );
1391                myMap.geoObjects.add(marker);
1392            }
1393        });
1394
1395        // 3. Финиш — зелёная метка
1396        var endCoords = obj.data.options.arrival_points;
1397        if (isValidCoords(endCoords)) {
1398            var endMarker = new ymaps.Placemark(
1399                endCoords,
1400                {
1401                    balloonContentHeader: "Финиш",
1402                    balloonContentBody: obj.data.options.arr_name,
1403                    hintContent: "Окончание: " + obj.data.options.arr_name
1404                },
1405                {
1406                    preset: 'islands#greenIcon'
1407                }
1408            );
1409            myMap.geoObjects.add(endMarker);
1410        }
1411    }
1412
1413    // Масштабирование карты под все метки
1414    function fitMapToMarkers() {
1415
1416        if (!myMap) return;
1417
1418        var bounds = myMap.geoObjects.getBounds();
1419
1420        if (bounds) {
1421            myMap.setBounds(bounds, {
1422                checkZoomRange: true,
1423                zoomMargin: 40
1424            });
1425        }
1426
1427    }
1428
1429    // Проверка координат
1430    function isValidCoords(coords) {
1431        return Array.isArray(coords) 
1432            && coords.length === 2 
1433            && typeof coords[0] === 'number' 
1434            && typeof coords[1] === 'number'
1435            && !isNaN(coords[0]) && !isNaN(coords[1]);
1436    }
1437
1438    // ===== ОСНОВНОЙ ЗАПРОС =====
1439    const fd = new FormData();
1440    fd.append("tour", id_tour);
1441
1442    postData("../user/getRoadPointJSON.php", fd, true, "GET").then(obj => {
1443        
1444      // Создаём интерфейс
1445      const bgHeight = document.documentElement.scrollHeight + "px";
1446      
1447      // фон
1448      const winBg = document.createElement("div");
1449      winBg.classList.add("win-bg");
1450      winBg.style.height = bgHeight;
1451      document.body.appendChild(winBg);
1452      winBg.classList.add("visible"); // плавное появление через CSS
1453
1454      // контейнер карты
1455      const winDiv = document.createElement("div");
1456      winDiv.id = "view_road";
1457      winDiv.style.opacity = "1";
1458      document.body.appendChild(winDiv);
1459      
1460      winDiv.innerHTML = "<span class='close'></span>";
1461
1462      // Закрытие
1463      winBg.addEventListener("click", () => {
1464          if (myMap) {
1465              myMap.geoObjects.removeAll();
1466              myMap.destroy();
1467              myMap = null;
1468          }
1469          winDiv.remove();
1470          winBg.remove();
1471      });
1472
1473    setTimeout(() => {
1474      winBg.classList.add("visible");
1475      document.querySelector("#view_road").classList.add("visible");
1476
1477      document.querySelector(".close").addEventListener("click", e => {
1478        if (myMap) {
1479            myMap.geoObjects.removeAll();
1480            myMap.destroy();
1481            myMap = null;
1482        }
1483        winDiv.remove();
1484        winBg.remove();        
1485      });      
1486    }, 10);
1487
1488      // Инициализация с проверкой API
1489      if (typeof ymaps === 'undefined') {
1490          alert('Яндекс.Карты не загружены');
1491          return;
1492      }
1493      
1494      ymaps.ready(() => {
1495          initRoadMap(obj);
1496      });
1497    }).catch(err => {
1498        console.error('Ошибка загрузки данных:', err);
1499        alert('Не удалось загрузить данные экскурсии');
1500    });
1501}
1502
1503function newTour_filtr()
1504{
1505  let loc = "/tours/region_" + document.querySelector("select[name=region]").value;
1506  if (document.querySelector("select[name=region]").value == "all") loc = "/tours/" + document.querySelector("select[name=region]").value;
1507    
1508  const dateInput = document.querySelector("input[name=tdate]");
1509  const dateValue = dateInput.dataset.value;
1510
1511  if (dateValue) {
1512    loc += "?date=" + dateValue;
1513  }
1514  
1515  window.location.href = loc;
1516}
1517function getCalendar(date, id, fs, fnct, $foreng = 0)
1518{
1519    document.body.style.cursor = "progress";
1520    //Определяем что за блок затребовал календарь
1521    var pafrentBlock = document.querySelector("div.myCalendar").parentElement;
1522    const fd = new FormData();
1523    fd.append("data_type", "tourCalendarMonth");
1524    fd.append("date", date);
1525    fd.append("id", id);
1526    fd.append("fs", fs);
1527    fd.append("fnct", fnct);
1528    fd.append("foreng", $foreng);
1529    
1530    postData("../user/getAjaxData.php", fd, false, "GET").then(data => {
1531        pafrentBlock.innerHTML = data;
1532        document.body.style.cursor = "auto";
1533    });
1534}
1535function saveData(menu, fb, e)
1536{
1537  if (e) e.preventDefault();
1538
1539  let fd, valid, form, uphone_value, inputs;
1540  OPTIONS.reload = false;
1541  
1542  switch(menu)
1543  {
1544    case "reserve":
1545
1546      valid = true;
1547      form = document.querySelector("form[name=ticket_count]");
1548      inputs = form.querySelectorAll("input");
1549      
1550      inputs.forEach(input => {
1551        if (!Validator.validate(input)) {
1552          valid = false;
1553        }
1554      });
1555
1556      let all_tic_count = recalcSum();
1557
1558      if (!all_tic_count) {
1559        const html = `
1560        <div class="win-close" onclick="closeFlyWindow();"></div>
1561        <img src="../images/errrrrr.jpg" />
1562        <h2 style="text-align: center;">Обратите внимание</h2>
1563        <p style="text-align: center;">
1564            Вы не выбрали количество билетов!<br>Посещение экскурсии проводится по заранее забронированным местам.
1565        </p>
1566        <button onclick="closeFlyWindow('booking-tcount');">Выбрать билеты</button>`;
1567
1568        createFlyWindow(html);
1569        return false;
1570      } else {
1571        const html = `<div class="win-close" onclick="closeFlyWindow();"></div>
1572          <div class='msg'>
1573              <div id='ajax'>
1574                  <div class='loader_a'></div>
1575                  <div class='loader_b'></div>
1576              </div>
1577              <p>Отправляем заявку ...</p>
1578          </div>`;
1579        createFlyWindow(html);        
1580      }
1581
1582      const p_pixel = (tour_id, cost) => {
1583          _tmr.push({ type: 'reachGoal', id: 3318706, value: cost, goal: 'purchase', params: { product_id: tour_id } });
1584      };
1585      p_pixel(form.querySelector("input[name=id_tour]").value, form.querySelector("input[name=pay_sum]").value);
1586
1587      const p_urlObj = new URL(window.location.href);
1588      const p_title_en = p_urlObj.pathname.split('/').pop(); // Получаем имя тура
1589
1590      document.body.style.cursor = "progress";
1591      var p_saveData = new FormData(form);
1592
1593      p_saveData.append("data", "reserve");
1594      p_saveData.append("title_en", p_title_en);
1595
1596      postData("../user/saveData.php", p_saveData).then(response => {
1597        if (!response.ok) {
1598          const html = `
1599            <div class="win-close" onclick="closeFlyWindow();"></div>
1600            <img src="../images/site-errror.jpg" />
1601            <h2 style="text-align: center">Ошибка резервирования</h2>
1602            <p>${response.error || response}</p>
1603            <button onclick="closeFlyWindow();">Хорошо</button>
1604            `;
1605          createFlyWindow(html);
1606        } else {
1607          const html = `
1608            <div class="win-close" onclick="closeFlyWindow();"></div>
1609            <img src="../images/zvonok1.jpg" />
1610            <h2 style="text-align: center">Резервирование успешно завершено</h2>
1611            <p>На почту отправлено письмо с копией заказа. Наш менеджер свяжется с Вами в ближайшее время.</p>
1612            <button onclick="closeFlyWindow();">Хорошо</button>
1613            `;
1614          createFlyWindow(html, true);
1615        }
1616      });
1617
1618      break;
1619    
1620    case "order":
1621
1622      valid = true;
1623      form = document.querySelector("form[name=ticket_count]");
1624      inputs = form.querySelectorAll("input");
1625      
1626      inputs.forEach(input => {
1627        if (!Validator.validate(input)) {
1628          valid = false;
1629        }
1630      });
1631
1632      // ==== Контроль времени закрытия тура ====
1633
1634      // время закрытия в минутах
1635      const closeTime = parseInt(form.close_time_minutes.value, 10);
1636
1637      // выбранные дата и время
1638      const tourDateText = document.querySelector("#itog_data").textContent.trim();
1639      const tourTimeText = document.querySelector("#itog_time").textContent.trim();
1640
1641      // 05.05.2026
1642      // 10:30
1643
1644      // разбираем дату
1645      const [day, month, year] = tourDateText.split('.');
1646      const [hours, minutes] = tourTimeText.split(':');
1647
1648      // создаём объект даты экскурсии
1649      const tourDateTime = new Date(
1650          year,
1651          month - 1, // JS месяцы с нуля
1652          day,
1653          hours,
1654          minutes
1655      );
1656
1657      // текущее время + буфер закрытия
1658      const now = new Date();
1659
1660      const closeBorder = new Date(
1661          now.getTime() + closeTime * 60 * 1000
1662      );
1663
1664      // проверка
1665      const canOrder = closeBorder <= tourDateTime;
1666
1667      console.log("Тур:", tourDateTime);
1668      console.log("Сейчас+буфер:", closeBorder);
1669      console.log("Можно бронировать:", canOrder);
1670
1671      // если нужно запретить
1672      if (!canOrder) {
1673        const html = `
1674        <div class="win-close" onclick="closeFlyWindow();"></div>
1675        <img src="../images/errrrrr.jpg" />
1676        <h2 style="text-align: center;">Обратите внимание</h2>
1677        <p style="text-align: center;">
1678            Онлайн продажа на это время уже закрыта. Выберите другое время или свяжитесь с нами по телефону: <a class="phone" href="tel:89990443323">+7 (999) 044-33-23</a>
1679        </p>
1680        <button onclick="closeFlyWindow('user-data');">Выбрать время</button>`;
1681
1682        createFlyWindow(html);
1683        return false;
1684      }
1685
1686
1687      let all_t_count = recalcSum();
1688      if(!valid){
1689        const html = `
1690        <div class="win-close" onclick="closeFlyWindow();"></div>
1691        <img src="../images/errrrrr.jpg" />
1692        <h2 style="text-align: center;">Обратите внимание</h2>
1693        <p style="text-align: center;">
1694            Заполните Ваши контактные данные.<br> Обратите внимание на подсказки сайта, указанные в полях.
1695        </p>
1696        <button onclick="closeFlyWindow('user-data');">Исправить</button>`;
1697
1698        createFlyWindow(html);
1699        return false;
1700      } else if (!all_t_count) {
1701        const html = `
1702        <div class="win-close" onclick="closeFlyWindow();"></div>
1703        <img src="../images/errrrrr.jpg" />
1704        <h2 style="text-align: center;">Обратите внимание</h2>
1705        <p style="text-align: center;">
1706            Вы не выбрали количество билетов!<br>Посещение экскурсии проводится по заранее забронированным местам.
1707        </p>
1708        <button onclick="closeFlyWindow('booking-tcount');">Выбрать билеты</button>`;
1709
1710        createFlyWindow(html);
1711        return false;
1712      } else {
1713        const html = `<div class="win-close" onclick="closeFlyWindow();"></div>
1714          <div class='msg'>
1715              <div id='ajax'>
1716                  <div class='loader_a'></div>
1717                  <div class='loader_b'></div>
1718              </div>
1719              <p>Перенаправление на платеж ...</p>
1720          </div>`;
1721        createFlyWindow(html);        
1722      }
1723
1724      const pixel = (tour_id, cost) => {
1725          _tmr.push({ type: 'reachGoal', id: 3318706, value: cost, goal: 'purchase', params: { product_id: tour_id } });
1726      };
1727      pixel(form.querySelector("input[name=id_tour]").value, form.querySelector("input[name=pay_sum]").value);
1728
1729      const urlObj = new URL(window.location.href);
1730      const title_en = urlObj.pathname.split('/').pop(); // Получаем имя тура
1731
1732      document.body.style.cursor = "progress";
1733      var saveData = new FormData(form);
1734
1735      saveData.append("data", "order");
1736      saveData.append("title_en", title_en);
1737
1738      postData("../user/saveData.php", saveData).then(response => {
1739        if (!response.ok) {
1740          const html = `
1741            <div class="win-close" onclick="closeFlyWindow();"></div>
1742            <img src="../images/site-errror.jpg" />
1743            <h2 style="text-align: center">Не получается купить</h2>
1744            <p>${response.error || response}</p>
1745            <button onclick="closeFlyWindow();">Хорошо</button>
1746            `;
1747          createFlyWindow(html);
1748        } else {
1749            window.location.href = response.href;
1750        }
1751      });
1752
1753      break;
1754    
1755    case "send-callbackQuery":
1756      
1757      form = document.callbackQuery;
1758      inputs = form.querySelectorAll("input");
1759
1760      valid = true;
1761
1762      inputs.forEach(input => {
1763        if (!Validator.validate(input)) {
1764          valid = false;
1765        }
1766      });
1767
1768      if(!valid){
1769        return false;
1770      }
1771
1772      uphone_value = form.user_phone.value;
1773
1774      fd = new FormData(form);
1775      fd.append("data", "send_callbackQuery");
1776
1777      const html = `<div class="win-close" onclick="closeFlyWindow();"></div>
1778        <div class='msg'>
1779            <div id='ajax'>
1780                <div class='loader_a'></div>
1781                <div class='loader_b'></div>
1782            </div>
1783            <p>Отправка сообщения ...</p>
1784        </div>`;
1785      createFlyWindow(html);
1786
1787      postData("../user/saveData.php", fd).then(response => {
1788        try {
1789          if (response.ok) {
1790            const flyWindow = `
1791            <div class="win-close" onclick="closeFlyWindow();"></div>
1792            <img src="../images/zvonok1.jpg" />
1793            <h2 style="text-align: center;">Спасибо за обращение</h2>
1794            <p style="text-align: center;">
1795                Спасибо за Вашу заявку, мы перезвоним Вам на номер
1796                <span style="display: block; text-align: center; font-size: 1.5em; color: #fd736c; margin: 0.5em;">
1797                ${uphone_value}
1798                </span>
1799                в ближайшее время.
1800            </p>
1801            <button onclick="closeFlyWindow();">Хорошо</button>
1802            `;
1803            createFlyWindow(flyWindow);
1804          } else {
1805            const flyWindow = `
1806            <div class="win-close" onclick="closeFlyWindow();"></div>
1807            <img src="../images/site-errror.jpg" />
1808            <p style="text-align: center;">
1809                <p>Ошибка отправки сообщения: ${response}</p>
1810            </p>
1811            <button onclick="closeFlyWindow();">Хорошо</button>
1812            `;
1813            createFlyWindow(flyWindow);
1814          }
1815        } catch (err) {
1816            console.error("call_back " + response, err);
1817        }
1818
1819      });
1820
1821      break;
1822    
1823    case "send_wish":
1824      
1825      form = document.callbackWhish;
1826      inputs = form.querySelectorAll("input");
1827
1828      valid = true;
1829
1830      inputs.forEach(input => {
1831        if (!Validator.validate(input)) {
1832          valid = false;
1833        }
1834      });
1835
1836      if(!valid){
1837        return false;
1838      }
1839
1840      uphone_value = form.user_phone.value;
1841
1842      fd = new FormData(form);
1843      fd.append("data", "send_wish");
1844
1845      const w_html = `<div class="win-close" onclick="closeFlyWindow();"></div>
1846        <div class='msg'>
1847            <div id='ajax'>
1848                <div class='loader_a'></div>
1849                <div class='loader_b'></div>
1850            </div>
1851            <p>Отправляем запрос ...</p>
1852        </div>`;
1853      createFlyWindow(w_html);
1854
1855      postData("../user/saveData.php", fd).then(response => {
1856        try {
1857          if (response.ok) {
1858            const flyWindow = `
1859            <div class="win-close" onclick="closeFlyWindow();"></div>
1860            <img src="../images/zvonok1.jpg" />
1861            <h2 style="text-align: center;">Спасибо за обращение</h2>
1862            <p style="text-align: center;">
1863                Спасибо за Вашу заявку, мы перезвоним Вам на номер
1864                <span style="display: block; text-align: center; font-size: 1.5em; color: #fd736c; margin: 0.5em;">
1865                ${uphone_value}
1866                </span>
1867                в ближайшее время.
1868            </p>
1869            <button onclick="closeFlyWindow();">Хорошо</button>
1870            `;
1871            createFlyWindow(flyWindow);
1872            document.callbackWhish.reset();
1873          } else {
1874            const flyWindow = `
1875            <div class="win-close" onclick="closeFlyWindow();"></div>
1876            <img src="../images/site-errror.jpg" />
1877            <p style="text-align: center;">
1878                <p>Ошибка отправки сообщения: ${response}</p>
1879            </p>
1880            <button onclick="closeFlyWindow();">Хорошо</button>
1881            `;
1882            createFlyWindow(flyWindow);
1883          }
1884        } catch (err) {
1885            console.error("call_back " + response, err);
1886        }
1887
1888      });
1889
1890      break;
1891    
1892    case "send-callback":
1893
1894      form = document.callback;
1895      const cb__phone = document.querySelector("input[name=callback_uphone]");
1896      uphone_value = cb__phone.value;
1897
1898      valid = true;
1899      if(!Validator.validate(cb__phone)){
1900        valid = false;
1901      }
1902
1903      if(!valid){
1904        return false;
1905      }
1906
1907      var formData = new FormData(form);
1908      formData.append("data", "call_back");
1909    
1910      const cb_html = `<div class="win-close" onclick="closeFlyWindow();"></div>
1911        <div class='msg'>
1912            <div id='ajax'>
1913                <div class='loader_a'></div>
1914                <div class='loader_b'></div>
1915            </div>
1916            <p>Отправляем запрос ...</p>
1917        </div>`;
1918      createFlyWindow(cb_html);
1919    
1920      postData("../user/saveData.php", formData).then(response => {
1921          try {
1922              if (response.ok) {
1923                  const html = `
1924                  <div class="win-close" onclick="closeFlyWindow();"></div>
1925                  <img src="../images/zvonok1.jpg" />
1926                  <h2 style="text-align: center;">Спасибо за обращение</h2>
1927                  <p style="text-align: center;">
1928                      Мы перезвоним Вам на номер
1929                      <span style="display: block; text-align: center; font-size: 1.5em; color: #fd736c; margin: 0.5em;">
1930                      ${uphone_value}
1931                      </span>
1932                      в ближайшее время.
1933                  </p>
1934                  <button onclick="closeFlyWindow();">Хорошо</button>
1935                  `;
1936                  createFlyWindow(html);
1937              } else {
1938                const flyWindow = `
1939                <div class="win-close" onclick="closeFlyWindow();"></div>
1940                <img src="../images/site-errror.jpg" />
1941                <p style="text-align: center;">
1942                    <p>Ошибка отправки сообщения: ${response}</p>
1943                </p>
1944                <button onclick="closeFlyWindow();">Хорошо</button>
1945                `;
1946                createFlyWindow(flyWindow);
1947              }
1948          } catch (err) {
1949              console.error("call_back " + response, err);
1950          }
1951        });
1952
1953      break;
1954      
1955      case "reit":
1956
1957        var formReit = document.querySelector("form[name=reit]");
1958        var uphone = formReit.querySelector("input[name=uphone]").value;
1959        var uname = formReit.querySelector("input[name=uname]").value;
1960        var responseText = formReit.querySelector(`[name="response"]`).value;
1961
1962        if (uname.length === 0) {
1963            alert("Пожалуйста укажите Ваше имя");
1964            formReit.querySelector("input[name=uname]").focus();
1965            return;
1966        }
1967
1968        if (responseText.length === 0) {
1969            alert("Пожалуйста напишите Ваш отзыв.");
1970            formReit.querySelector("input[name=response]").focus();
1971            return;
1972        }
1973
1974        if (uphone.length !== 0) {
1975            if (!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/.test(uphone)) {
1976                if (!/^[\+\d\s]{0,3}\({0,1}\d{3,4}[\)\s]{0,2}[\d\-\s]{7,10}$/.test(uphone)) {
1977                    alert("Пожалуйста введите e-mail или контактный телефон.");
1978                    formReit.querySelector("input[name=uphone]").focus();
1979                    return;
1980                }
1981            }
1982        } else {
1983            alert("Пожалуйста введите e-mail или контактный телефон.");
1984            formReit.querySelector("input[name=uphone]").focus();
1985            return;
1986        }
1987
1988        var saveDataReit = new FormData(formReit);
1989        saveDataReit.append("data", "reit");
1990
1991        postData("../user/saveData.php", saveDataReit).then(response => {
1992            if (response.ok) {
1993                const html = `
1994                  <div class="win-close" onclick="closeFlyWindow();"></div>
1995                  <img src="../images/box-faq-image.webp" />
1996                  <h2 style="text-align: center;">Спасибо за Ваш отзыв</h2>
1997                  <p style="text-align: center;">
1998                    Мы благодарны, что Вы позволяете сделать наши экскурсии лучше. Отзыв будет опубликован после модерации.
1999                  </p>
2000                  <button onclick="closeFlyWindow();">Хорошо</button>
2001                  `;
2002                  createFlyWindow(html);
2003              formReit.reset();
2004            } else {
2005                const flyWindow = `
2006                <div class="win-close" onclick="closeFlyWindow();"></div>
2007                <img src="../images/site-errror.jpg" />
2008                <p style="text-align: center;">
2009                    <p>Ошибка отправки сообщения: ${response}</p>
2010                </p>
2011                <button onclick="closeFlyWindow();">Хорошо</button>
2012                `;
2013                createFlyWindow(flyWindow);
2014            }
2015        });
2016
2017      break;
2018
2019  }
2020  
2021
2022}
2023function fillOptionList(list) {
2024  switch (list) {
2025    case "place":
2026      const region = document.querySelector("form[name=tour_filters] input[name=region]").value;
2027
2028      const fd = new FormData();
2029      fd.append("data_type", "option_list");
2030      fd.append("list", list);
2031      fd.append("region", region);
2032
2033      postData("../user/getAjaxData.php", fd, false, "GET").then(response => {
2034        const select_place = document.querySelector("form[name=tour_filters] select[name=place]");
2035        if (!select_place) return;
2036
2037        // очистить старые option
2038        select_place.innerHTML = "";
2039
2040        // добавить "Все достопримечательности"
2041        const optAll = document.createElement("option");
2042        optAll.textContent = "Все достопримечательности";
2043        optAll.value = "all";
2044        optAll.selected = true;
2045        select_place.appendChild(optAll);
2046
2047          // добавить остальные
2048          try {
2049              console.log("fillOptionList JSON:", response);
2050              const data = JSON.parse(response);
2051              data.forEach(value => {
2052                  const opt = document.createElement("option");
2053                  opt.textContent = value[1];
2054                  opt.value = value[0];
2055                  select_place.appendChild(opt);
2056              });
2057          }
2058          catch (err) {
2059              console.error("fillOptionList error", err);
2060          }
2061      });
2062      break;
2063  }
2064}
2065function createFlyWindow(htmlInside, reload = false) {
2066
2067  OPTIONS.reload = reload;
2068  OPTIONS.justOpenedWindow = true;
2069  let winDiv = document.querySelector(".window");
2070  
2071  if (!winDiv) {
2072    const bgHeight = document.documentElement.scrollHeight + "px";
2073    const winBg = document.createElement("div");
2074    winBg.classList.add("win-bg");
2075    winBg.style.height = bgHeight;
2076    document.body.appendChild(winBg);
2077    winBg.classList.add("visible");
2078
2079    winDiv = document.createElement("div");
2080    winDiv.classList.add("window");
2081    document.body.appendChild(winDiv);
2082  }
2083
2084  const flyWindow = `
2085    <div class="win-close" onclick="closeFlyWindow();"></div>
2086    ${htmlInside}`;
2087  winDiv.innerHTML = flyWindow;
2088
2089  if (!winDiv.classList.contains("visible")) {
2090    winDiv.classList.add("visible");
2091  }
2092
2093  // сбрасываем флаг после завершения текущего цикла событий
2094  setTimeout(() => {
2095    OPTIONS.justOpenedWindow = false;
2096  }, 0);
2097}
2098
2099function closeFlyWindow(scroll_hash = "") {
2100  const winDiv = document.querySelector("div.window");
2101  const winBg = document.querySelector("div.win-bg");
2102
2103  if (winDiv) {
2104    winDiv.remove();
2105  }
2106
2107  if (winBg) {
2108    winBg.remove();
2109  }
2110
2111  // Скролим, если указали куда
2112  if (scroll_hash.length != 0) {
2113    scrollToAnchor(scroll_hash);
2114  }
2115  // Перегружаем, если указано
2116  if (OPTIONS.reload) {
2117    window.location.reload();
2118  }
2119}
2120async function postData(func, formData = null, json = true, method = "POST") {
2121  let options = { method };
2122
2123  if (method === "POST" && formData) {
2124    options.body = formData;
2125  }
2126
2127  if (method === "GET" && formData) {
2128    const params = new URLSearchParams(formData).toString();
2129    func += (func.includes("?") ? "&" : "?") + params;
2130  }
2131
2132  const response = await fetch(func, options);
2133  const text = await response.text();
2134
2135  if (json) {
2136    try {
2137      return JSON.parse(text);
2138    } catch (err) {
2139      console.error("Ошибка парсинга JSON:", err, "Сырой ответ:", text);
2140      return text; // возвращаем строку, чтобы не терять выполнение
2141    }
2142  }
2143
2144  return text;
2145}
2146
2147
2148function getCookie(name) {
2149  let cookies = document.cookie.split("; ");
2150  for (let c of cookies) {
2151    let [key, value] = c.split("=");
2152    if (key === name) return decodeURIComponent(value);
2153  }
2154  return undefined;
2155}
2156function checkCookie() {
2157    document.cookie = "userCookie=check; path=/; max-age=3600";
2158    let cBlock = document.querySelector("div#cookieBlock");
2159    cBlock.style.display = "none";
2160}
2161async function load_lazy_scripts() {
2162
2163  if (lazyScriptsLoaded) return;
2164  lazyScriptsLoaded = true;
2165
2166  const scripts = await postData("/user/lazy_scripts.php", new FormData(), true);
2167
2168  scripts.forEach(item => {
2169
2170    let code = item.code ? decodeHtml(item.code) : "";
2171
2172    // пропускаем карту если не tour
2173    if (!window.location.pathname.startsWith("/tour") && code.includes("api-maps")) {
2174      return;
2175    }
2176
2177    // внешний src
2178    if (item.src) {
2179
2180      if ([...document.scripts].some(s => s.src === item.src)) {
2181        return;
2182      }
2183
2184      const s = document.createElement("script");
2185      s.src = item.src;
2186      s.async = true;
2187      document.head.appendChild(s);
2188
2189    }
2190
2191    // inline
2192    else if (code) {
2193
2194      // защита от повторной вставки
2195      if (document.body.innerHTML.includes(code.substring(0,50))) {
2196        return;
2197      }
2198
2199      injectCode(code);
2200
2201    }
2202
2203  });
2204
2205}
2206
2207function injectCode(html) {
2208  const parser = new DOMParser();
2209  const doc = parser.parseFromString(html, "text/html");
2210
2211  doc.querySelectorAll("script").forEach(scr => {
2212    const s = document.createElement("script");
2213    if (scr.src) {
2214      s.src = scr.src.trim(); // важно: убрать пробелы!
2215      s.async = true;
2216    } else {
2217      s.textContent = scr.textContent;
2218    }
2219    document.head.appendChild(s); // head предпочтительнее для метрик
2220  });
2221}
2222function decodeHtml(html) {
2223  const txt = document.createElement("textarea");
2224  txt.innerHTML = html;
2225  return txt.value;
2226}
2227function toHex(bytes) {
2228    return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(' ');
2229}
2230const observer = new IntersectionObserver(entries => {
2231  entries.forEach(entry => {
2232    if (entry.isIntersecting) {
2233      const el = entry.target;
2234      el.style.backgroundImage = `url(${el.dataset.bg})`;
2235      observer.unobserve(el);
2236    }
2237  });
2238});
2239
2240document.querySelectorAll('.lazy-bg').forEach(el => {
2241  observer.observe(el);
2242});
2243
2244function FeedBackSlider() {
2245  const s = document.querySelector('.box-reviews__slider');
2246  const b = document.querySelector('.box-reviews__block');
2247  if (!s) return;
2248  const w = s.querySelector('.box-reviews__wrapper'),
2249        prev = b.querySelector('.box-reviews__prev'),
2250        next = b.querySelector('.box-reviews__next'),
2251        slides = s.querySelectorAll('.box-reviews__slide');
2252  
2253  let i = 0, per = 3, startX = 0, isDrag = false;
2254  
2255  const getPer = () => window.innerWidth < 768 ? 1 : window.innerWidth < 1200 ? 2 : 3;
2256  
2257  const move = () => {
2258    const slideEl = slides[0];
2259
2260    if (!slideEl) return;
2261    
2262    const wrapperStyle = getComputedStyle(w);
2263    
2264    // Берём gap из wrapper (если используется)
2265    const gap = parseInt(wrapperStyle.gap) || 0;
2266    
2267    // Полная ширина шага: слайд + отступ после него
2268    const slideWidthPx = slideEl.offsetWidth;
2269    const stepWidth = slideWidthPx + gap;
2270    
2271    w.style.transform = `translateX(-${i * stepWidth}px)`;
2272  };
2273  
2274  const update = () => { 
2275    per = getPer(); 
2276    i = Math.min(i, Math.max(0, slides.length - per)); 
2277    move(); 
2278  };
2279  
2280  prev?.addEventListener('click', () => { 
2281    if (i > 0) { i--; move(); } 
2282  });
2283  
2284  next?.addEventListener('click', () => { 
2285    const maxI = Math.max(0, slides.length - per);
2286    if (i < maxI) { i++; move(); } 
2287  });
2288  
2289  // Touch/Mouse swipe
2290  const onS = (e) => { 
2291    startX = e.touches?.[0]?.clientX ?? e.clientX; 
2292    isDrag = true; 
2293  };
2294  
2295  const onM = (e) => {
2296    if (!isDrag) return;
2297    const x = e.touches?.[0]?.clientX ?? e.clientX;
2298    const diff = startX - x;
2299    if (Math.abs(diff) > 50) {
2300      const maxI = Math.max(0, slides.length - per);
2301      if (diff > 0 && i < maxI) { i++; move(); }
2302      else if (diff < 0 && i > 0) { i--; move(); }
2303      isDrag = false;
2304    }
2305  };
2306  
2307  const onE = () => isDrag = false;
2308  
2309  s.addEventListener('touchstart', onS, { passive: true });
2310  s.addEventListener('touchmove', onM, { passive: true });
2311  s.addEventListener('touchend', onE);
2312  s.addEventListener('mousedown', onS);
2313  s.addEventListener('mousemove', onM);
2314  s.addEventListener('mouseup', onE);
2315  s.addEventListener('mouseleave', onE);
2316  
2317  window.addEventListener('resize', update);
2318  update();
2319}
2320function bindEvents() {
2321
2322  if (document._myEventsBound) return;
2323  document._myEventsBound = true;
2324  
2325  // Привязываем обработчики событий ко всему документу
2326  Object.keys(eventHandlers).forEach((eventType) => {
2327    document.addEventListener(eventType, (event) => {
2328        
2329      // Кнопки +- на билетах
2330      const cmdsPlus = event.target.closest(".quantity__button.quantity__button--plus");
2331      if (cmdsPlus) {
2332        let input = cmdsPlus.closest("[data-quantity]").querySelector("input[name^=tickets]");
2333        input.value++;
2334        recalcSum();
2335      }
2336
2337      const cmdsMinus = event.target.closest(".quantity__button.quantity__button--minus");
2338      if (cmdsMinus) {
2339        let input = cmdsMinus.closest("[data-quantity]").querySelector("input[name^=tickets]");
2340        input.value--;
2341        if (input.value < 0) input.value = 0;
2342        recalcSum();
2343      }
2344
2345      // Ищем ближайший элемент с атрибутом data-run-script="true"
2346      const targetElement = event.target.closest('[data-run-script="true"]');
2347      if (!targetElement) return;
2348
2349      // Проверяем, соответствует ли текущий тип события ожидаемому
2350      const dataEvent = (targetElement.dataset.event || 'click').split(/\s+/).map(e => e.trim());
2351
2352      if (!dataEvent.includes(eventType)) return;
2353      eventHandlers[eventType](event, targetElement);
2354
2355    }, true);
2356  });
2357}
2358// Объект для хранения событий и их обработчиков
2359const eventHandlers = {
2360    click: handleBindEvents,
2361};
2362function handleBindEvents(event, element) {
2363
2364  let template, clone, form;
2365  
2366    try {
2367        const scriptData = element.dataset.script;
2368        if (!scriptData) {
2369            console.error('Элемент не содержит data-script');
2370            return;
2371        }
2372
2373        const { script, param } = JSON.parse(decodeURIComponent(scriptData));
2374
2375      switch (script) {
2376
2377        case ("logoff"):
2378
2379          event.preventDefault();
2380
2381          let logofData = new FormData();
2382          logofData.append("data", "p_logoff");
2383
2384          postData("../user/saveData.php", logofData).then(response => {
2385            if (response.ok) {
2386              window.location.reload();
2387            }
2388          });
2389          
2390          break;
2391        
2392        case ("login"):
2393          
2394          let loginForm = document.logon;
2395          let inputs = loginForm.querySelectorAll("input");
2396
2397          let valid = true;
2398
2399          inputs.forEach(input => {
2400            if (!Validator.validate(input)) {
2401              valid = false;
2402            }
2403          });
2404
2405          if(!valid){
2406            return false;
2407          }
2408
2409          let logonData = new FormData(loginForm);
2410          logonData.append("data", "p_logon");
2411
2412          postData("../user/saveData.php", logonData).then(response => {
2413            if (!response.ok) {
2414              const html = `
2415                <div class="win-close" onclick="closeFlyWindow();"></div>
2416                <img src="../images/site-errror.jpg" />
2417                <h2 style="text-align: center">Ошибка входа</h2>
2418                <p>${response.message || response}</p>
2419                <button onclick="closeFlyWindow();">Хорошо</button>
2420                `;
2421              createFlyWindow(html);
2422            } else {
2423              window.location.reload();
2424            }
2425          });
2426          
2427          break;
2428        
2429        case ("parthner-logon"):
2430
2431          // Окно входя для партнеров
2432          event.preventDefault();
2433
2434          let logon = `
2435            <form name="logon" onsubmit="return false;"><div class="logon">
2436              <label>Логин
2437                <div class="field">
2438                <input class="bottom-booking__input" type="text" name="login" value="" data-required data-validate-text="Поле обязательное">
2439                </div>
2440              </label>
2441              <label>Пароль
2442              <div class="field">
2443              <input class="bottom-booking__input" name="password" type="password" value="" data-required data-validate-text="Поле обязательное">
2444              </div>
2445              </label>
2446              <button data-run-script="true" data-script="%7B%22script%22%3A%22login%22%2C%22param%22%3A%7B%7D%7D">Вход</button>     
2447            </div></form>`;
2448          
2449          createFlyWindow(logon);
2450
2451          break;
2452        
2453        case ("check_personalData"):
2454          
2455          // Проверяем, что клик по ссылке
2456          let link = event.target.closest('a');
2457          if (link) {
2458            event.preventDefault();
2459            let href = link.getAttribute("href");
2460            window.open(href);
2461            return;
2462          }
2463
2464          // Принимаю   
2465          let checkPD = event.target.closest("div").querySelector("input[type=checkbox]");
2466          
2467          if (checkPD) {
2468            let cmd = checkPD.closest(".box-body").querySelector("button");
2469            if (checkPD.checked) {
2470              cmd.disabled = false;
2471            } else {
2472              cmd.disabled = true;
2473            }     
2474          }
2475
2476          break;
2477        
2478        case ("show-feedback"):
2479          
2480          let article = `
2481            <div class="win-close" onclick="closeFlyWindow();"></div>
2482            <p>${param.user_name}</p>
2483            <div class="box-reviews__rating">
2484              <div class="box-reviews__stars">${drawRatingStars(param.reit)}</div>
2485              <div class="box-reviews__value">${param.reit}</div>
2486            </div>
2487            <p>${param.article}</p>
2488            <button onclick="closeFlyWindow();">Закрыть</button>`;
2489
2490          createFlyWindow(article);
2491          break;
2492
2493        case ("reserve"):
2494
2495          event.preventDefault();
2496          saveData("reserve");
2497
2498          break;
2499        
2500        case ("gotoBank"):
2501
2502          event.preventDefault();
2503          saveData("order");
2504
2505          break;
2506        
2507        case ("callbackQuery"):
2508
2509          event.preventDefault();
2510          template = document.getElementById('callbackForm');
2511          clone = template.content.cloneNode(true);
2512          form = clone.querySelector('form');
2513          createFlyWindow(form.outerHTML);
2514
2515          break;
2516        
2517        case ("send-wish"):
2518
2519          event.preventDefault();
2520          saveData("send_wish");
2521
2522          break;
2523        
2524        case ("send-callback"):
2525        case ("send-callbackQuery"):
2526
2527          event.preventDefault();
2528          saveData(script);
2529          
2530          break;
2531
2532        case ("callback"):
2533
2534          event.preventDefault();
2535          template = document.getElementById('callback');
2536          clone = template.content.cloneNode(true);
2537          form = clone.querySelector('form');
2538          createFlyWindow(form.outerHTML);
2539
2540          break;
2541
2542        case ("scroll-article"):
2543          // Скролим при сворачивании статьи до видимости кнопки
2544          let check = document.querySelector(param.checkbox).checked;
2545
2546          if (check) {
2547            setTimeout(() => {
2548              let cmd = document.querySelector(".box-program");
2549              let coo = cmd.getBoundingClientRect();
2550
2551              window.scrollTo({
2552                top: window.scrollY + coo.top + coo.height - 800,
2553                behavior: "smooth"
2554              });              
2555            }, 100);
2556          }
2557          
2558          break;
2559        
2560        case "setDate":
2561
2562          // если у нас время выключено, то ничего не делаем
2563          if (element.classList.contains("disabled_date")) {
2564            return false;
2565          }
2566
2567          const foreng = document.querySelector("input[name=foreng]");
2568          const id_time = document.querySelector("input[name=id_time]");
2569          const datePlace = document.querySelector("#itog_data");
2570          const timePlace = document.querySelector("#itog_time");
2571          const startPoint = document.querySelector("#itog_startpoint");
2572
2573          const timeBlocksUp = document.querySelector("#thisStartPointUp");
2574          const timeBlocksDown = document.querySelector("#thisStartPointDown");
2575
2576          const crossBlocksUp = document.querySelector("#crossStartPointUp");
2577          const crossBlocksDown = document.querySelector("#crossStartPointDown");
2578
2579          const dateSpInput = document.querySelector("input[name=sp_time]");
2580          const strtPointInput = document.querySelector("input[name=start_point]");
2581
2582          const transp = document.querySelector("input[name=transp]");
2583          const close_time_minutes = document.querySelector("input[name=close_time_minutes]");
2584          
2585          const spBlock = element.closest("div[data-startpoint]");
2586
2587          const fullPayInput = document.querySelectorAll("input[name=fullPay]");
2588          const options__item = document.querySelectorAll(".options__item");
2589
2590          dateSpInput.value = param.date;
2591          strtPointInput.value = spBlock.dataset.startpoint;
2592          startPoint.textContent = spBlock.dataset.startpoint;
2593          id_time.value = param.id;
2594          transp.value = spBlock.dataset.transp;
2595          close_time_minutes.value = spBlock.dataset.closetime;
2596            
2597          let updateMap = false;
2598          //Ключ обновления карты
2599          if (foreng.value != param.foreng) {
2600            updateMap = true;
2601          }
2602          foreng.value = param.foreng;
2603
2604          let picketDate = new Date(param.date);
2605
2606          datePlace.textContent = picketDate.toLocaleString('ru-RU', {
2607            day: '2-digit',
2608            month: '2-digit',
2609            year: 'numeric'
2610          });
2611
2612          timePlace.textContent = picketDate.toLocaleString('ru-RU', {
2613            hour: '2-digit',
2614            minute: '2-digit'
2615          });
2616
2617          if (param.foreng == 0) {
2618            if (fullPayInput.length != 0) {
2619              fullPayInput[0].checked = true;
2620              options__item[1].style.display = "block";
2621            }
2622
2623            // Удалить все метки времени на внешней точке
2624            if (crossBlocksDown) {
2625              [crossBlocksUp, crossBlocksDown].forEach(timeBlock => {
2626                let timeCmds = timeBlock.querySelectorAll("button");
2627                if (timeCmds) {
2628                  timeCmds.forEach(timeCmd => {
2629                    timeCmd.classList.remove("selected_date");
2630                  });
2631                }
2632              });
2633            }
2634
2635            // Сделать метку времени
2636            if (timeBlocksUp) {
2637              [timeBlocksUp, timeBlocksDown].forEach(timeBlock => {
2638                let timeCmds = timeBlock.querySelectorAll("button");
2639                if (timeCmds) {
2640                  timeCmds.forEach(timeCmd => {
2641                    if (timeCmd.dataset.timeid == param.id) {
2642                      timeCmd.classList.add("selected_date");
2643                    } else {
2644                      timeCmd.classList.remove("selected_date");
2645                    }
2646                  });
2647                }
2648              });
2649            }
2650          } else {
2651            if (fullPayInput.length != 0) {
2652              fullPayInput[0].checked = true;
2653              options__item[1].style.display = "none";
2654            }
2655            if (timeBlocksUp) {
2656              [timeBlocksUp, timeBlocksDown].forEach(timeBlock => {
2657                let timeCmds = timeBlock.querySelectorAll("button");
2658                if (timeCmds) {
2659                  timeCmds.forEach(timeCmd => {
2660                    timeCmd.classList.remove("selected_date");
2661                  });
2662                }
2663              });
2664            }
2665
2666            if (crossBlocksUp) {
2667              [crossBlocksUp, crossBlocksDown].forEach(timeBlock => {
2668                let timeCmds = timeBlock.querySelectorAll("button");
2669                if (timeCmds) {
2670                  timeCmds.forEach(timeCmd => {
2671                    if (timeCmd.dataset.timeid == param.id) {
2672                      timeCmd.classList.add("selected_date");
2673                    } else {
2674                      timeCmd.classList.remove("selected_date");
2675                    }
2676                  });
2677                }
2678              });
2679            }
2680          }
2681
2682          //Обновим карту по ключу
2683          if (updateMap) {
2684            const ya_map_input = document.querySelector("input[name=ya_map]");
2685            ya_map_input.value = spBlock.dataset.map;
2686              
2687            // Преобразуем координаты в массив
2688            let ya_map = spBlock.dataset.map.split(",");
2689            const lat = ya_map[0].trim();
2690            const lon = ya_map[1].trim();
2691
2692            let img = document.querySelector("img#static_ymap");
2693            img.src = `https://static-maps.yandex.ru/1.x/?ll=${lon},${lat}&z=15&l=map&pt=${lon},${lat},pm2al&size=328,328`;
2694
2695          }
2696            
2697          break;
2698
2699        }
2700
2701    } catch (error) {
2702        console.error('Ошибка обработки команды:', error);
2703    }    
2704}
2705
2706// наблюдаем видимость блока покупки
2707const booking_block = document.querySelector('#booking');
2708const scroll_observer = new IntersectionObserver((entries) => {
2709
2710  entries.forEach(entry => {
2711
2712    if(entry.isIntersecting){
2713      OPTIONS.booking_block_visible = true;
2714    } else {
2715      OPTIONS.booking_block_visible = false;
2716    }
2717
2718  });
2719
2720}, {
2721  threshold: 0.2
2722});
2723
2724if (booking_block) {
2725  scroll_observer.observe(booking_block);
2726}
2727
2728function isVisible(el){
2729  return el.offsetParent !== null;
2730}
2731function drawRatingStars(rating) {
2732    // Нормализуем рейтинг: число от 0 до 5
2733    rating = Math.max(0, Math.min(5, parseFloat(rating) || 0));
2734    
2735    const fullStars = Math.floor(rating);           // Целая часть (полные звёзды)
2736    const hasHalfStar = (rating - fullStars) > 0;   // Есть ли дробная часть
2737    const emptyStarsCount = 5 - Math.ceil(rating);  // Пустые звёзды до 5
2738    
2739    // Уникальный ID для clipPath, чтобы избежать конфликтов при множественных вызовах
2740    const uniqueId = 'half-star-' + Math.random().toString(36).substr(2, 9);
2741    
2742    let response = '';
2743    
2744    // SVG: полная звезда
2745    const fullStarSVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2746        <rect width="24" height="24" fill="white"/>
2747        <path d="M11.9998 17.75L5.82784 20.995L7.00684 14.122L2.00684 9.25495L8.90684 8.25495L11.9928 2.00195L15.0788 8.25495L21.9788 9.25495L16.9788 14.122L18.1578 20.995L11.9998 17.75Z"
2748            fill="#FFCC02" stroke="#FFCC02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2749    </svg>`;
2750    
2751    // SVG: половина звезды (с clipPath)
2752    const halfStarSVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2753        <defs>
2754            <clipPath id="${uniqueId}">
2755                <rect x="0" y="0" width="12" height="24"/>
2756            </clipPath>
2757        </defs>
2758        <rect width="24" height="24" fill="white"/>
2759        <path d="M11.9998 17.75L5.82784 20.995L7.00684 14.122L2.00684 9.25495L8.90684 8.25495L11.9928 2.00195L15.0788 8.25495L21.9788 9.25495L16.9788 14.122L18.1578 20.995L11.9998 17.75Z"
2760            fill="none" stroke="#FFCC02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2761        <path d="M11.9998 17.75L5.82784 20.995L7.00684 14.122L2.00684 9.25495L8.90684 8.25495L11.9928 2.00195L15.0788 8.25495L21.9788 9.25495L16.9788 14.122L18.1578 20.995L11.9998 17.75Z"
2762            fill="#FFCC02" stroke="none" clip-path="url(#${uniqueId})"/>
2763    </svg>`;
2764    
2765    // SVG: пустая звезда
2766    const emptyStarSVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2767        <rect width="24" height="24" fill="white"/>
2768        <path d="M11.9998 17.75L5.82784 20.995L7.00684 14.122L2.00684 9.25495L8.90684 8.25495L11.9928 2.00195L15.0788 8.25495L21.9788 9.25495L16.9788 14.122L18.1578 20.995L11.9998 17.75Z"
2769            fill="transparent" stroke="#FFCC02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
2770    </svg>`;
2771    
2772    // Добавляем полные звёзды
2773    for (let i = 0; i < fullStars; i++) {
2774        response += fullStarSVG;
2775    }
2776    
2777    // Добавляем половину звезды, если есть дробная часть
2778    if (hasHalfStar) {
2779        response += halfStarSVG;
2780    }
2781    
2782    // Добавляем пустые звёзды
2783    for (let i = 0; i < emptyStarsCount; i++) {
2784        response += emptyStarSVG;
2785    }
2786    
2787    return response;
2788}
2789// Делегирование на document для динамической валидации полей
2790const ValidatorModule = (() => {
2791  document.addEventListener('blur', (e) => {
2792    const field = e.target;
2793    if (field.matches('input[data-validate-text]')) {
2794      Validator?.validate(field);
2795    }
2796  }, true); // true = capture phase, чтобы ловить blur
2797
2798  return {
2799    // Можно добавить методы для переинициализации
2800    reinit: () => {
2801      console.log('Валидация активна для всех полей, включая динамические');
2802    }
2803  };
2804})();
2805function scrollToAnchor(id) {
2806  let more = 200; // Опустить немного ниже
2807
2808    const el = document.getElementById(id);
2809    if (!el) return;
2810
2811    const y = el.getBoundingClientRect().top + window.pageYOffset;
2812
2813    window.scrollTo({
2814        top: y - more,
2815        behavior: 'smooth'
2816    });
vendor: 1 bytes, line 2817
2817}

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.