(async function () {
var DEFAULT_API_BASE = "https://samo.evroport.ru/vendor/evroport/api/index.php";
var mountNode = window.__ETOURS_TARGET || document.getElementById("etours");
if (!mountNode) {
mountNode = document.querySelector('[data-widget-name="etours-extsearch"]');
}
if (!mountNode) {
return;
}
window.__ETOURS_TARGET = null;
function resolveAssetBase() {
var current = document.currentScript;
if (current && current.src) {
return current.src.replace(/[^/]*$/, "");
}
var scripts = document.getElementsByTagName("script");
for (var i = scripts.length - 1; i >= 0; i -= 1) {
var src = scripts[i].src || "";
if (/extsearch\.js(\?|$)/.test(src)) {
return src.replace(/[^/]*$/, "");
}
}
return "./";
}
function ensureStyle(href) {
var existing = document.querySelector('link[data-etours-style="' + href + '"]');
if (existing) {
return;
}
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.setAttribute("data-etours-style", href);
document.head.appendChild(link);
}
function ensureExtsearchCss(assetBase, version) {
var baseHref = assetBase + "extsearch.css";
var href = baseHref + (version ? ("?v=" + encodeURIComponent(version)) : ("?v=" + Date.now()));
var existing = document.querySelector('link[data-etours-style="' + baseHref + '"]');
if (existing) {
if (existing.getAttribute("href") !== href) {
existing.href = href;
}
return;
}
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.setAttribute("data-etours-style", baseHref);
document.head.appendChild(link);
}
function darkenHexColor(hex, factor) {
var value = String(hex || "").trim();
var match = value.match(/^#([0-9a-f]{6})$/i);
if (!match) {
return "";
}
var base = match[1];
var ratio = typeof factor === "number" ? factor : 0.18;
var r = Math.max(0, Math.min(255, Math.round(parseInt(base.slice(0, 2), 16) * (1 - ratio))));
var g = Math.max(0, Math.min(255, Math.round(parseInt(base.slice(2, 4), 16) * (1 - ratio))));
var b = Math.max(0, Math.min(255, Math.round(parseInt(base.slice(4, 6), 16) * (1 - ratio))));
return "#" + [r, g, b].map(function (n) {
var h = n.toString(16);
return h.length === 1 ? "0" + h : h;
}).join("");
}
function loadScript(src) {
return new Promise(function (resolve, reject) {
if (document.querySelector('script[data-etours-script="' + src + '"]')) {
resolve();
return;
}
var script = document.createElement("script");
script.src = src;
script.async = false;
script.setAttribute("data-etours-script", src);
script.onload = function () {
resolve();
};
script.onerror = function () {
reject(new Error("Не удалось загрузить скрипт: " + src));
};
document.head.appendChild(script);
});
}
async function ensureDependencies(assetBase) {
var hadEtoursJQ = !!(window.etoursJQ && window.etoursJQ.fn && window.etoursJQ.fn.dropdown && window.etoursJQ.fn.checkbox);
if (hadEtoursJQ) {
return;
}
var prevJQ = window.jQuery;
var prevDollar = window.$;
await loadScript(assetBase + "static/js/jquery-3.7.1.min.js");
await loadScript(assetBase + "static/js/core.js");
await loadScript(assetBase + "static/js/touch.js");
await loadScript(assetBase + "static/js/scrollbar.js");
await loadScript(assetBase + "static/js/dropdown.js");
await loadScript(assetBase + "static/js/checkbox.js");
await loadScript(assetBase + "static/js/calendar-availability.js");
if (window.jQuery && typeof window.jQuery.noConflict === "function") {
window.etoursJQ = window.jQuery.noConflict(true);
}
if (prevJQ) {
window.jQuery = prevJQ;
}
if (prevDollar) {
window.$ = prevDollar;
}
}
function buildWidgetMarkup(apiBaseUrl) {
var safeApiBase = String(apiBaseUrl || "").replace(/"/g, """);
return (
'
' +
'
" +
'
Поиск завершен на 0% - подождите немного, мы подбираем для Вас лучшие цены...
' +
"
"
);
}
var assetBase = resolveAssetBase();
var widgetVersionEarly = mountNode.getAttribute("data-widget-version") || "1.0.8";
ensureStyle(assetBase + "static/css/dropdown.css");
ensureStyle(assetBase + "static/css/checkbox.css");
ensureStyle(assetBase + "static/css/number.css");
ensureStyle(assetBase + "static/css/scrollbar.css");
ensureStyle(assetBase + "static/css/calendar-availability.css");
ensureStyle(assetBase + "static/datepicker.css");
ensureStyle(assetBase + "static/searchform.css");
ensureExtsearchCss(assetBase, widgetVersionEarly);
await ensureDependencies(assetBase);
document.body.classList.add("etours-extsearch");
var apiBase = mountNode.getAttribute("data-api-base") || DEFAULT_API_BASE;
var agentEmail = mountNode.getAttribute("data-agent-email") || "";
var widgetName = mountNode.getAttribute("data-widget-name") || "etours-extsearch";
var widgetVersion = mountNode.getAttribute("data-widget-version") || "1.0.0";
var statHost = window.location && window.location.hostname ? String(window.location.hostname) : "localhost";
if (!statHost) {
statHost = "localhost";
}
var brandColor = mountNode.getAttribute("data-brand-color") || "";
var brandDark = mountNode.getAttribute("data-brand-dark") || darkenHexColor(brandColor, 0.2);
var topColor = mountNode.getAttribute("data-top-color") || "";
var topDark = mountNode.getAttribute("data-top-dark") || darkenHexColor(topColor, 0.14);
var textLight = mountNode.getAttribute("data-text-light") || "";
var buttonText = mountNode.getAttribute("data-button-text") || "";
var showCurrenciesRaw = String(mountNode.getAttribute("data-show-currencies") || "1").toLowerCase();
var showCurrencies = !(showCurrenciesRaw === "0" || showCurrenciesRaw === "false" || showCurrenciesRaw === "no" || showCurrenciesRaw === "off");
var formModeRaw = String(mountNode.getAttribute("data-form-mode") || "short").toLowerCase();
var defaultAdvancedOpen =
formModeRaw === "full" ||
formModeRaw === "полная" ||
formModeRaw === "1" ||
formModeRaw === "true" ||
formModeRaw === "yes";
var roundedButtonsRaw = String(mountNode.getAttribute("data-rounded-buttons") || "0").toLowerCase();
var roundedButtons =
roundedButtonsRaw === "1" ||
roundedButtonsRaw === "true" ||
roundedButtonsRaw === "yes" ||
roundedButtonsRaw === "on";
// Страны/города вылета: allowlist (выбрать). Пустой список = все.
// Старые data-blacklist-departure-* ещё читаем как exclude для совместимости.
var allowDepartureCountriesRaw =
mountNode.getAttribute("data-allow-departure-countries") ||
mountNode.getAttribute("data-whitelist-departure-countries") ||
"";
var allowDepartureCitiesRaw =
mountNode.getAttribute("data-allow-departure-cities") ||
mountNode.getAttribute("data-whitelist-departure-cities") ||
"";
var legacyBlacklistDepartureCountriesRaw = mountNode.getAttribute("data-blacklist-departure-countries") || "";
var legacyBlacklistDepartureCitiesRaw = mountNode.getAttribute("data-blacklist-departure-cities") || "";
var blacklistTourCountriesRaw = mountNode.getAttribute("data-blacklist-tour-countries") || "";
var defaultDepartureCountry = (mountNode.getAttribute("data-default-departure-country") || "").trim();
var defaultDepartureCity = (mountNode.getAttribute("data-default-departure-city") || "").trim();
var defaultTourCountry = (mountNode.getAttribute("data-default-tour-country") || "").trim();
mountNode.innerHTML = buildWidgetMarkup(apiBase);
var wrapper = mountNode.querySelector(".sf-wrapper");
if (wrapper && !showCurrencies) {
var currenciesBlock = wrapper.querySelector(".sf-currencies");
if (currenciesBlock) {
currenciesBlock.style.display = "none";
currenciesBlock.setAttribute("hidden", "hidden");
}
wrapper.classList.add("etw-no-currencies");
}
if (wrapper && roundedButtons) {
wrapper.classList.add("etw-rounded-buttons");
}
if (wrapper) {
if (brandColor) {
wrapper.style.setProperty("--etw-color-brand", brandColor);
}
if (brandDark) {
wrapper.style.setProperty("--etw-color-brand-dark", brandDark);
}
if (topColor) {
wrapper.style.setProperty("--etw-color-top", topColor);
}
if (topDark) {
wrapper.style.setProperty("--etw-color-top-dark", topDark);
}
if (textLight) {
wrapper.style.setProperty("--etw-color-text-light", textLight);
}
if (buttonText) {
wrapper.style.setProperty("--etw-color-button-text", buttonText);
}
}
function setGlobalVar(name, value) {
if (!value) {
return;
}
document.documentElement.style.setProperty(name, value);
if (document.body) {
document.body.style.setProperty(name, value);
}
}
setGlobalVar("--etw-color-brand", brandColor);
setGlobalVar("--etw-color-brand-dark", brandDark);
setGlobalVar("--etw-color-top", topColor);
setGlobalVar("--etw-color-top-dark", topDark);
setGlobalVar("--etw-color-text-light", textLight);
setGlobalVar("--etw-color-button-text", buttonText);
var q = function (selector) {
return wrapper ? wrapper.querySelector(selector) : null;
};
var form = q("#priceSearchForm");
if (!form) {
throw new Error("Не удалось инициализировать форму поиска: #priceSearchForm не найден.");
}
var searchButton = form.querySelector(".sf-search-button");
var tableContainer = mountNode.querySelector(".sf-searchresult__table");
var loadingBox = mountNode.querySelector(".sf-searchresult__loading");
var progressNode = q("#sf-status__loader-bar-count");
var progressTextNode = q("#sf-status__loader-bar-text");
var countryFromNode = q("#countryfrom");
var depCityNode = q("#depCity");
var countryNode = q("#country");
var tourCityNode = q("#tourCity");
var tourTypeNode = q("#tourType");
var tourNode = q("#tour");
var mealNode = q("#meal");
var hotelStarsNode = q("#hotelStars");
var hotelSelectNode = q("#hotelSelect");
var hotelFilterNode = q("#hotelFilter");
var nightsFromNode = q("#nightsFrom");
var nightsToNode = q("#nightsTo");
var dateFromNode = q("#dateFrom");
var dateToNode = q("#dateTo");
var childsNode = q("#childs");
var child1AgeNode = q("#child1Age");
var child2AgeNode = q("#child2Age");
var childInBedNode = q("#childInBed");
var hasFlightSeatsNode = q("#hasFlightSeats");
var noStopSaleNode = q("#noStopSale");
var momentConfirmNode = q("#momentConfirm");
var advancedFiltersNode = q("#advancedFilters");
var advancedFiltersBox = q("#etwAdvancedFilters");
if (advancedFiltersNode) {
advancedFiltersNode.checked = defaultAdvancedOpen;
}
if (advancedFiltersBox) {
advancedFiltersBox.classList.toggle("sf-advanced-filters_hidden", !defaultAdvancedOpen);
if (defaultAdvancedOpen) {
advancedFiltersBox.removeAttribute("hidden");
} else {
advancedFiltersBox.setAttribute("hidden", "hidden");
}
}
if (wrapper) {
wrapper.classList.toggle("etw-form-short", !defaultAdvancedOpen);
wrapper.classList.toggle("etw-form-full", defaultAdvancedOpen);
}
var adultsNode = q("#adults");
var priceFromNode = q("#priceFrom");
var priceToNode = q("#priceTo");
var priceModeNodes = form.querySelectorAll('input[name="ncr"]');
var jq = window.etoursJQ || null;
var fullTownFroms = [];
var countryFromMap = {};
var hotelCatalog = [];
var tourTypeCatalog = [];
var renderResultMap = {};
var pendingShareMultiSelect = null;
var currentSearchPage = 1;
var lastSearchPager = { current: 1, total: 1 };
var controlsRequestToken = 0;
var lastCheckinList = null;
var rateUsdNode = wrapper ? wrapper.querySelector("#rateUsd") : null;
var rateEurNode = wrapper ? wrapper.querySelector("#rateEur") : null;
var ratesDateNode = wrapper ? wrapper.querySelector("#ratesDate") : null;
var dateHintsNode = wrapper ? wrapper.querySelector("#etwDateHints") : null;
var nightsHintsNode = wrapper ? wrapper.querySelector("#etwNightsHints") : null;
var calendarAvailability = null;
function parseList(raw) {
return String(raw || "")
.split(",")
.map(function (value) {
return String(value || "").trim();
})
.filter(Boolean);
}
var allowDepartureCountries = parseList(allowDepartureCountriesRaw);
var allowDepartureCities = parseList(allowDepartureCitiesRaw);
var legacyBlacklistDepartureCountries = parseList(legacyBlacklistDepartureCountriesRaw);
var legacyBlacklistDepartureCities = parseList(legacyBlacklistDepartureCitiesRaw);
var blacklistTourCountries = parseList(blacklistTourCountriesRaw);
// Пустой allow и нет старого blacklist → только Россия.
if (!allowDepartureCountries.length && !legacyBlacklistDepartureCountries.length) {
allowDepartureCountries = ["2"];
}
function isDepartureCountryAllowed(countryId) {
var id = String(countryId || "");
if (!id) {
return false;
}
if (allowDepartureCountries.length) {
return allowDepartureCountries.indexOf(id) >= 0;
}
if (legacyBlacklistDepartureCountries.length) {
return legacyBlacklistDepartureCountries.indexOf(id) < 0;
}
return true;
}
function isDepartureCityAllowed(cityId, countryId) {
var id = String(cityId || "");
if (!id || !isDepartureCountryAllowed(countryId)) {
return false;
}
if (allowDepartureCities.length) {
return allowDepartureCities.indexOf(id) >= 0;
}
// Город по умолчанию без списка «выбрать города» = только этот город.
if (defaultDepartureCity) {
return id === String(defaultDepartureCity);
}
if (legacyBlacklistDepartureCities.length) {
return legacyBlacklistDepartureCities.indexOf(id) < 0;
}
return true;
}
if (window.EtwCalendarAvailability) {
calendarAvailability = new window.EtwCalendarAvailability({
dateNode: dateHintsNode,
nightsNode: nightsHintsNode,
onRangeSelect: function (range) {
var from = range && range.from ? String(range.from) : "";
var to = range && range.to ? String(range.to) : from;
dateFromNode.value = from;
dateToNode.value = to;
hideDateHints();
refreshControls().catch(function () {});
}
});
}
function buildApiUrl(action, params) {
var url = new URL(apiBase, window.location.href);
url.searchParams.set("action", action);
Object.keys(params || {}).forEach(function (key) {
var value = params[key];
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}
async function fetchJSON(url) {
var response = await fetch(url);
var text = await response.text();
var payload = null;
try {
payload = JSON.parse(text);
} catch (err) {
throw new Error("API вернул не JSON");
}
if (!response.ok || payload.ok === false) {
throw new Error((payload && payload.message) || "Ошибка API");
}
return payload;
}
function fillSelect(select, items, defaultLabel, valueKey, labelKey) {
select.innerHTML = "";
if (defaultLabel) {
var defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = defaultLabel;
select.appendChild(defaultOption);
}
items.forEach(function (item) {
var option = document.createElement("option");
option.value = String(item[valueKey || "id"]);
option.textContent = item[labelKey || "name"];
select.appendChild(option);
});
}
function getSelectedValues(selectNode) {
return Array.prototype.slice
.call(selectNode.options || [])
.filter(function (opt) {
return opt.selected && opt.value !== "";
})
.map(function (opt) {
return String(opt.value);
});
}
function fillMultiSelect(selectNode, items, selectedValues, valueKey, labelKey) {
var selectedMap = {};
(selectedValues || []).forEach(function (value) {
selectedMap[String(value)] = true;
});
var hasManualSelection = Object.keys(selectedMap).length > 0;
selectNode.innerHTML = "";
(items || []).forEach(function (item) {
var option = document.createElement("option");
option.value = String(item[valueKey || "id"]);
option.textContent = item[labelKey || "name"];
option.selected = !!selectedMap[option.value] || (!hasManualSelection && Number(item.selected) === 1);
selectNode.appendChild(option);
});
}
function updateFormstoneSelect(selectNode, hardReset) {
if (selectNode && selectNode.getAttribute && selectNode.getAttribute("data-no-formstone") === "true") {
return;
}
if (!jq || !jq.fn || !jq.fn.dropdown) {
return;
}
var el = jq(selectNode);
var isMultiple = !!(selectNode && selectNode.multiple);
if (el.data("fsDropdown") && hardReset) {
el.dropdown("destroy");
el.dropdown({ selectMultiple: isMultiple });
return;
}
if (el.data("fsDropdown")) {
el.dropdown("update");
return;
}
el.dropdown({ selectMultiple: isMultiple });
}
function refreshAllFormstoneSelects(hardReset) {
if (!wrapper) {
return;
}
wrapper.querySelectorAll(".sf-field__select").forEach(function (selectNode) {
updateFormstoneSelect(selectNode, !!hardReset);
});
}
function isAdvancedFiltersOpen() {
return !!(advancedFiltersNode && advancedFiltersNode.checked);
}
function setAdvancedFiltersVisible(isOpen) {
var open = !!isOpen;
if (advancedFiltersNode) {
advancedFiltersNode.checked = open;
if (jq && jq.fn && jq.fn.checkbox) {
try {
jq(advancedFiltersNode).checkbox("update");
} catch (err) {}
}
}
if (advancedFiltersBox) {
advancedFiltersBox.classList.toggle("sf-advanced-filters_hidden", !open);
if (open) {
advancedFiltersBox.removeAttribute("hidden");
} else {
advancedFiltersBox.setAttribute("hidden", "hidden");
}
}
if (wrapper) {
wrapper.classList.toggle("etw-form-short", !open);
wrapper.classList.toggle("etw-form-full", open);
}
if (open) {
refreshAllFormstoneSelects(false);
}
}
function initFormstoneInputs() {
if (!jq || !jq.fn) {
return;
}
if (jq.fn.dropdown) {
jq(".sf-field__select").each(function () {
updateFormstoneSelect(this);
});
}
if (jq.fn.number) {
jq(".sf-field__number").number();
}
if (jq.fn.checkbox) {
jq(".sf-field__checkbox, .sf-field__radio").checkbox();
}
}
function fillNightsOptions(nights, places) {
nightsFromNode.innerHTML = "";
nightsToNode.innerHTML = "";
var placesMap = {};
(places || []).forEach(function (value) {
placesMap[String(value)] = true;
});
var hasPlaces = Object.keys(placesMap).length > 0;
(nights || []).forEach(function (value) {
var key = String(value);
var isAvailable = hasPlaces ? !!placesMap[key] : true;
var fromOption = document.createElement("option");
fromOption.value = key;
fromOption.textContent = key + (isAvailable ? "" : " (по запросу)");
fromOption.disabled = !isAvailable;
nightsFromNode.appendChild(fromOption);
var toOption = document.createElement("option");
toOption.value = key;
toOption.textContent = key + (isAvailable ? "" : " (по запросу)");
toOption.disabled = !isAvailable;
nightsToNode.appendChild(toOption);
});
}
function fillNightsFromObject(nightsObject) {
var nights = [];
var places = [];
var defaultFrom = "";
var defaultTill = "";
if (Array.isArray(nightsObject)) {
nights = nightsObject;
} else if (nightsObject && Array.isArray(nightsObject.nights)) {
nights = nightsObject.nights;
places = Array.isArray(nightsObject.places) ? nightsObject.places : [];
defaultFrom = nightsObject.default && nightsObject.default.from ? String(nightsObject.default.from) : "";
defaultTill = nightsObject.default && nightsObject.default.till ? String(nightsObject.default.till) : "";
}
fillNightsOptions(nights, places);
if (defaultFrom) {
nightsFromNode.value = defaultFrom;
}
if (defaultTill) {
nightsToNode.value = defaultTill;
}
if ((!nightsFromNode.value || (nightsFromNode.selectedOptions[0] && nightsFromNode.selectedOptions[0].disabled)) && nightsFromNode.options.length) {
var firstEnabledFrom = Array.prototype.find.call(nightsFromNode.options, function (opt) { return !opt.disabled; });
if (firstEnabledFrom) {
nightsFromNode.value = String(firstEnabledFrom.value);
} else {
nightsFromNode.selectedIndex = 0;
}
}
if ((!nightsToNode.value || (nightsToNode.selectedOptions[0] && nightsToNode.selectedOptions[0].disabled)) && nightsToNode.options.length) {
var enabledTo = Array.prototype.filter.call(nightsToNode.options, function (opt) { return !opt.disabled; });
if (enabledTo.length) {
nightsToNode.value = String(enabledTo[enabledTo.length - 1].value);
} else {
nightsToNode.selectedIndex = nightsToNode.options.length - 1;
}
}
if (calendarAvailability) {
calendarAvailability.updateNights(nights, places);
}
}
function getSelectFieldRoot(selectNode) {
if (!selectNode) {
return null;
}
if (selectNode.closest) {
var byClosest = selectNode.closest(".sf-field");
if (byClosest) {
return byClosest;
}
}
var node = selectNode.parentNode;
while (node && node !== wrapper && node !== document.body) {
if (node.classList && node.classList.contains("sf-field")) {
return node;
}
node = node.parentNode;
}
return null;
}
function setSelectFieldCaption(selectNode, text) {
var root = getSelectFieldRoot(selectNode);
if (!root) {
return;
}
var caption = root.querySelector(".sf-field__caption");
if (caption) {
caption.textContent = text;
}
}
function setSelectFieldVisible(selectNode, visible) {
var root = getSelectFieldRoot(selectNode);
if (!root) {
return;
}
root.classList.toggle("is-auto-hidden", !visible);
}
function getSelectedOptionLabel(selectNode) {
if (!selectNode || selectNode.selectedIndex < 0) {
return "";
}
var opt = selectNode.options[selectNode.selectedIndex];
return opt && opt.value ? String(opt.textContent || "").trim() : "";
}
function selectHasOption(selectNode, value) {
var id = String(value || "");
if (!selectNode || !id) {
return false;
}
return Array.prototype.some.call(selectNode.options || [], function (opt) {
return String(opt.value || "") === id;
});
}
function countValuedOptions(selectNode) {
if (!selectNode) {
return 0;
}
return Array.prototype.slice.call(selectNode.options || []).filter(function (opt) {
return String(opt.value || "") !== "";
}).length;
}
function ensureSoleValuedOptionSelected(selectNode) {
if (!selectNode) {
return "";
}
var valued = Array.prototype.slice.call(selectNode.options || []).filter(function (opt) {
return String(opt.value || "") !== "";
});
if (valued.length !== 1) {
return selectNode.value ? String(selectNode.value) : "";
}
var onlyId = String(valued[0].value);
if (String(selectNode.value || "") !== onlyId) {
selectNode.value = onlyId;
}
return onlyId;
}
/**
* Formstone может показывать подпись города при пустом native value
* (если value выставили на id, которого нет в options — браузер сбрасывает в "").
* Перед search/controls всегда нормализуем TOWNFROMINC.
*/
function ensureDepartureCitySelected() {
if (countryFromNode) {
if (!countryFromNode.value) {
ensureSoleValuedOptionSelected(countryFromNode);
}
if (!countryFromNode.value) {
var preferredCountry = resolveDefaultDepartureCountryId(
Object.keys(countryFromMap || {}).map(function (key) {
return countryFromMap[key];
})
);
if (preferredCountry && selectHasOption(countryFromNode, preferredCountry)) {
countryFromNode.value = preferredCountry;
filterTownFromsByCountryFrom();
}
}
}
if (!depCityNode) {
return "";
}
var current = String(depCityNode.value || "");
if (current && selectHasOption(depCityNode, current)) {
return current;
}
var nextId = "";
if (defaultDepartureCity && selectHasOption(depCityNode, defaultDepartureCity)) {
nextId = String(defaultDepartureCity);
} else {
var valued = Array.prototype.slice.call(depCityNode.options || []).filter(function (opt) {
return String(opt.value || "") !== "";
});
if (valued.length === 1) {
nextId = String(valued[0].value);
} else if (valued.length > 0) {
nextId = String(valued[0].value);
}
}
if (nextId) {
depCityNode.value = nextId;
updateFormstoneSelect(depCityNode, true);
syncDepartureFieldPresentation();
}
return String(depCityNode.value || "");
}
var lastDepartureCountryCount = 0;
var lastDepartureCityCount = 0;
function syncDepartureFieldPresentation() {
var countryCount = lastDepartureCountryCount || countValuedOptions(countryFromNode);
var cityCount = lastDepartureCityCount || countValuedOptions(depCityNode);
if (countryCount === 1) {
ensureSoleValuedOptionSelected(countryFromNode);
}
if (cityCount === 1) {
ensureSoleValuedOptionSelected(depCityNode);
}
var countryName =
getSelectedOptionLabel(countryFromNode) ||
(countryFromNode.value && countryFromMap[String(countryFromNode.value)]
? countryFromMap[String(countryFromNode.value)].name
: "");
var cityName = getSelectedOptionLabel(depCityNode);
var hideCountry = countryCount === 1 && !!countryFromNode.value;
var hideCity = cityCount === 1 && !!depCityNode.value;
setSelectFieldVisible(countryFromNode, !hideCountry);
setSelectFieldCaption(countryFromNode, "Страна вылета");
setSelectFieldVisible(depCityNode, !hideCity);
if (hideCountry && countryName) {
setSelectFieldCaption(depCityNode, "Город вылета (" + countryName + ")");
} else {
setSelectFieldCaption(depCityNode, "Город вылета");
}
if (hideCity) {
var tourCaption = "Страна тура";
if (countryName && cityName) {
tourCaption = "Страна тура (" + countryName + ", из " + cityName + ")";
} else if (cityName) {
tourCaption = "Страна тура (из " + cityName + ")";
} else if (countryName) {
tourCaption = "Страна тура (" + countryName + ")";
}
setSelectFieldCaption(countryNode, tourCaption);
} else {
setSelectFieldCaption(countryNode, "Страна тура");
}
}
function buildCountryFromOptions(townFroms) {
var map = {};
(townFroms || []).forEach(function (town) {
var key = String(town.stateFromKey || "");
if (!key || !isDepartureCountryAllowed(key)) {
return;
}
if (!map[key]) {
map[key] = {
id: key,
name: town.stateFromName || ("Страна " + key)
};
}
});
countryFromMap = map;
var items = Object.keys(map).map(function (key) {
return map[key];
});
items.sort(function (a, b) {
return a.name.localeCompare(b.name, "ru");
});
lastDepartureCountryCount = items.length;
// Одна страна — без «Любая», сразу выбираем её.
fillSelect(countryFromNode, items, items.length === 1 ? "" : "Любая");
if (items.length === 1) {
countryFromNode.value = String(items[0].id);
} else {
var preferredCountryFrom = resolveDefaultDepartureCountryId(items);
if (preferredCountryFrom) {
countryFromNode.value = String(preferredCountryFrom);
}
}
updateFormstoneSelect(countryFromNode, true);
if (items.length === 1) {
ensureSoleValuedOptionSelected(countryFromNode);
updateFormstoneSelect(countryFromNode, false);
}
}
function resolveDefaultDepartureCountryId(items) {
var list = items || [];
if (defaultDepartureCountry) {
var byAttr = list.find(function (item) {
return String(item.id) === String(defaultDepartureCountry);
});
if (byAttr) {
return String(byAttr.id);
}
}
var byName = list.find(function (item) {
var name = String(item.name || "").toLowerCase();
return name === "россия" || name === "russia";
});
if (byName) {
return String(byName.id);
}
var byKnownId = list.find(function (item) {
return String(item.id) === "2";
});
return byKnownId ? String(byKnownId.id) : "";
}
function filterTownFromsByCountryFrom() {
var selectedCountryFrom = countryFromNode.value;
var currentTown = depCityNode.value;
var filtered = fullTownFroms.filter(function (town) {
var townId = String(town.id || "");
var countryId = String(town.stateFromKey || "");
if (!isDepartureCityAllowed(townId, countryId)) {
return false;
}
return !selectedCountryFrom || countryId === String(selectedCountryFrom);
});
fillSelect(depCityNode, filtered, filtered.length === 1 ? "" : "Любой");
lastDepartureCityCount = filtered.length;
if (filtered.length === 1) {
depCityNode.value = String(filtered[0].id);
} else {
if (currentTown) {
var existsInFiltered = filtered.some(function (town) {
return String(town.id) === String(currentTown);
});
if (existsInFiltered) {
depCityNode.value = currentTown;
}
}
if (!depCityNode.value && filtered.length > 0) {
if (defaultDepartureCity) {
var defaultTown = filtered.find(function (town) {
return String(town.id) === String(defaultDepartureCity);
});
if (defaultTown) {
depCityNode.value = String(defaultTown.id);
}
}
}
if (!depCityNode.value && filtered.length > 0) {
var selectedTown = filtered.find(function (town) {
return Number(town.selected) === 1;
});
depCityNode.value = String((selectedTown || filtered[0]).id);
}
}
updateFormstoneSelect(depCityNode, true);
if (filtered.length === 1) {
ensureSoleValuedOptionSelected(depCityNode);
updateFormstoneSelect(depCityNode, false);
}
syncDepartureFieldPresentation();
}
function bindSelectChange(node, handler) {
if (!node) {
return;
}
node.addEventListener("change", handler);
if (jq && jq.fn) {
jq(node).on("change.etours", handler);
}
}
function handleCascadeSelectChange(node) {
if (!node) {
return;
}
if (node === countryFromNode) {
hideDateHints();
filterTownFromsByCountryFrom();
refreshControls({ resetHotels: true }).catch(function () {});
return;
}
if (node === depCityNode || node === countryNode || node === tourCityNode || node === tourTypeNode || node === tourNode || node === mealNode || node === hotelStarsNode) {
hideDateHints();
refreshControls({ resetHotels: true }).catch(function () {});
return;
}
if (node === hotelSelectNode) {
hideDateHints();
}
}
function bindCascadeSelectHandlers() {
if (!wrapper || wrapper.__etwCascadeBound) {
return;
}
wrapper.__etwCascadeBound = true;
wrapper.addEventListener("change", function (event) {
var node = event.target;
if (!node || !node.classList || !node.classList.contains("sf-field__select")) {
return;
}
handleCascadeSelectChange(node);
});
if (jq && jq.fn) {
jq(wrapper).on("change.etours", ".sf-field__select", function () {
handleCascadeSelectChange(this);
});
}
}
function filterHotelOptions() {
rebuildHotelSelect();
}
function filterHotelsBySelections(items) {
var selectedTowns = getSelectedValues(tourCityNode);
var selectedStars = getSelectedValues(hotelStarsNode);
var pattern = String(hotelFilterNode.value || "").trim().toLowerCase();
return (items || []).filter(function (hotel) {
var townOk = selectedTowns.length === 0 || selectedTowns.indexOf(String(hotel.townKey || "")) >= 0;
var starOk = selectedStars.length === 0 || selectedStars.indexOf(String(hotel.starKey || "")) >= 0;
var nameOk = !pattern || String(hotel.name || "").toLowerCase().indexOf(pattern) >= 0;
return townOk && starOk && nameOk;
});
}
function rebuildHotelSelect(selectedHotelIds) {
var selected = selectedHotelIds || getSelectedValues(hotelSelectNode);
var visibleHotels = filterHotelsBySelections(hotelCatalog);
var hotelsWithStars = visibleHotels.map(function (item) {
var starLabel = item.star || item.starAlt || "";
// In SAMO Search_Api::_getPRICES() the booking token is returned as:
// id = Cat_Claim. Keep mapping strict to avoid passing wrong identifiers.
var catClaimValue = item.id || item.Key || "";
return {
id: item.id,
name: starLabel ? item.name + ", " + starLabel : item.name
};
});
fillMultiSelect(hotelSelectNode, hotelsWithStars, selected);
updateFormstoneSelect(hotelSelectNode, true);
}
function clearSelectSelection(selectNode) {
Array.prototype.slice.call(selectNode.options || []).forEach(function (option) {
option.selected = false;
});
}
function filterToursBySelectedType(tours) {
var selectedTypeId = String(tourTypeNode.value || "");
if (!selectedTypeId) {
return tours || [];
}
var selectedType = (tourTypeCatalog || []).find(function (type) {
return String(type.id) === selectedTypeId;
});
if (!selectedType || !Array.isArray(selectedType.tours) || selectedType.tours.length === 0) {
return tours || [];
}
var allowed = {};
selectedType.tours.forEach(function (tourId) {
allowed[String(tourId)] = true;
});
return (tours || []).filter(function (tour) {
return !!allowed[String(tour.id)];
});
}
function updateChildAgeState() {
var childrenCount = Number(childsNode.value || 0);
var child1Enabled = childrenCount >= 1;
var child2Enabled = childrenCount >= 2;
child1AgeNode.disabled = !child1Enabled;
child2AgeNode.disabled = !child2Enabled;
if (!child1Enabled) {
child1AgeNode.value = "0";
}
if (!child2Enabled) {
child2AgeNode.value = "0";
}
updateFormstoneSelect(child1AgeNode, true);
updateFormstoneSelect(child2AgeNode, true);
}
async function refreshControls(options) {
var opts = options || {};
var requestToken = ++controlsRequestToken;
var controls = "STATES,TOWNS,TOUR_TYPES,TOURS,MEALS,STARS,HOTELS,NIGHTS,CHECKIN_LIST";
var selectedTowns = isAdvancedFiltersOpen() ? getSelectedValues(tourCityNode) : [];
var selectedStars = isAdvancedFiltersOpen() ? getSelectedValues(hotelStarsNode) : [];
var selectedHotels = opts.resetHotels || !isAdvancedFiltersOpen() ? [] : getSelectedValues(hotelSelectNode);
var selectedMeal = isAdvancedFiltersOpen() ? getSelectedValues(mealNode) : [];
if (opts.resetHotels) {
clearSelectSelection(hotelSelectNode);
hotelFilterNode.value = "";
}
var params = {
controls: controls,
TOWNFROMINC: ensureDepartureCitySelected() || "",
STATEINC: countryNode.value || "",
TOWNS: selectedTowns.join(","),
TOURTYPEINC: tourTypeNode.value || "",
TOURINC: tourNode.value || "",
MEALS: selectedMeal.join(","),
STARS: selectedStars.join(","),
HOTELS: selectedHotels.join(","),
CHECKIN_BEG: toIsoDate(dateFromNode.value),
CHECKIN_END: toIsoDate(dateToNode.value),
NIGHTS_FROM: nightsFromNode.value || "",
NIGHTS_TILL: nightsToNode.value || ""
};
var payload = await fetchJSON(buildApiUrl("controls", params));
if (requestToken !== controlsRequestToken) {
return;
}
var data = payload.data || {};
var nextControls = data.controls || {};
var selectedCountry = countryNode.value;
var selectedTourCities = selectedTowns;
var selectedTourType = tourTypeNode.value;
var selectedTour = tourNode.value;
var requestedStateInc = String(params.STATEINC || "");
if (Array.isArray(nextControls.STATES)) {
var allowedStates = nextControls.STATES.filter(function (item) {
return blacklistTourCountries.indexOf(String(item.id || "")) < 0;
});
fillSelect(countryNode, allowedStates, "Любая");
if (selectedCountry && selectHasOption(countryNode, selectedCountry)) {
countryNode.value = selectedCountry;
}
if (!countryNode.value && defaultTourCountry) {
var hasDefaultTourCountry = allowedStates.some(function (item) {
return String(item.id) === String(defaultTourCountry);
});
if (hasDefaultTourCountry) {
countryNode.value = String(defaultTourCountry);
}
}
if (!countryNode.value) {
var selectedState = allowedStates.find(function (item) {
return Number(item.selected) === 1;
});
if (selectedState) {
countryNode.value = String(selectedState.id);
} else if (allowedStates.length > 0 && !defaultTourCountry) {
countryNode.value = String(allowedStates[0].id);
}
}
updateFormstoneSelect(countryNode, true);
}
if (Array.isArray(nextControls.TOUR_TYPES)) {
tourTypeCatalog = nextControls.TOUR_TYPES;
fillSelect(tourTypeNode, nextControls.TOUR_TYPES, "Любой");
if (selectedTourType && selectHasOption(tourTypeNode, selectedTourType)) {
tourTypeNode.value = selectedTourType;
}
updateFormstoneSelect(tourTypeNode, true);
}
if (Array.isArray(nextControls.TOURS)) {
var filteredTours = filterToursBySelectedType(nextControls.TOURS);
fillSelect(tourNode, filteredTours, "Любой");
if (selectedTour && selectHasOption(tourNode, selectedTour)) {
tourNode.value = selectedTour;
}
updateFormstoneSelect(tourNode, true);
}
if (Array.isArray(nextControls.MEALS)) {
fillMultiSelect(mealNode, nextControls.MEALS, selectedMeal);
updateFormstoneSelect(mealNode, true);
}
if (Array.isArray(nextControls.TOWNS)) {
fillMultiSelect(tourCityNode, nextControls.TOWNS, selectedTourCities);
updateFormstoneSelect(tourCityNode, true);
}
if (Array.isArray(nextControls.STARS)) {
var filteredStars = nextControls.STARS.filter(function (item) {
var match = String(item.name || item.nameAlt || "").match(/(\d+)\s*\*/);
if (!match) {
return false;
}
return Number(match[1]) >= 2;
});
fillMultiSelect(hotelStarsNode, filteredStars, selectedStars);
updateFormstoneSelect(hotelStarsNode, true);
}
if (Array.isArray(nextControls.HOTELS)) {
hotelCatalog = nextControls.HOTELS.map(function (item) {
return {
id: item.id,
name: item.name || "",
star: item.star || item.starAlt || "",
starAlt: item.starAlt || item.star || "",
starKey: item.starKey || "",
townKey: item.townKey || ""
};
});
rebuildHotelSelect(selectedHotels);
} else if (hotelCatalog.length) {
rebuildHotelSelect(selectedHotels);
}
if (nextControls.NIGHTS && typeof nextControls.NIGHTS === "object" && !nextControls.NIGHTS.error) {
fillNightsFromObject(nextControls.NIGHTS);
updateFormstoneSelect(nightsFromNode, true);
updateFormstoneSelect(nightsToNode, true);
}
if (nextControls.CHECKIN_LIST && calendarAvailability) {
var checkinPayload = nextControls.CHECKIN_LIST;
// SAMO отдаёт объект { date: { valid, start }, value, rangeCalendar }.
if (Array.isArray(checkinPayload)) {
checkinPayload = checkinPayload.length ? checkinPayload[0] : null;
}
if (checkinPayload && checkinPayload.date && (checkinPayload.date.valid || checkinPayload.date.start)) {
lastCheckinList = checkinPayload;
calendarAvailability.updateDates(checkinPayload);
calendarAvailability.setRange(dateFromNode.value, dateToNode.value);
}
}
// После автовыбора страны (default / selected / первая) списки типа тура,
// туров, городов и т.д. нужно перезапросить уже с STATEINC.
var finalStateInc = String(countryNode.value || "");
if (finalStateInc && finalStateInc !== requestedStateInc) {
await refreshControls(opts);
return;
}
refreshAllFormstoneSelects(false);
syncDepartureFieldPresentation();
}
function normalizePrices(payload) {
var root = payload && payload.data ? payload.data : {};
var prices = root.result && Array.isArray(root.result.prices) ? root.result.prices : [];
return prices.map(function (item) {
var isStopSale = Boolean(item.stopSale || item.stopSpog);
var isMomentConfirm = Boolean(item.momentConfirm || item.MomentConfirm || item.confirmNow || item.ConfirmNow);
var flightInCode = item.freights && item.freights.econom ? String(item.freights.econom.in || "") : "";
var flightOutCode = item.freights && item.freights.econom ? String(item.freights.econom.out || "") : "";
var hotelAvailabilityCode = String(item.hotelAvailability || "");
var baggage = item.freights && item.freights.econom ? String(item.freights.econom.baggage || "") : "";
var spoRaw = item.spo || item.SPO || "";
var priceRub = item.convertedPriceNumber || item.convertedPrice || item.price || "";
var priceTour = item.price || item.PriceCur || item.convertedPrice || "";
var tourCurrency = item.currencyAlias || item.currency || item.Currency || "";
var priceRubNumber = Number(item.convertedPriceNumber || 0);
var priceTourNumber = Number(item.price || 0);
var programLink = item.programLink || item.tourUrl || item.url || item.packetUrl || item.link || "";
var catClaimValue = item.id || item.Key || "";
return {
resultId: item.id || item.Key || "",
nights: item.nights || item.hnights || "",
country: item.state || item.country || "",
city: item.town || item.city || "",
departureCity: item.townFrom || "",
hotel: item.hotel || "",
star: item.star || item.starAlt || "",
room: item.room || "",
meal: item.meal || "",
tour: item.tour || item.title || "",
spoRaw: spoRaw,
hotelUrl: item.hotelUrl || item.HotelUrl || "",
dateFrom: item.checkIn || item.dateFrom || "",
dateTo: item.checkOut || item.dateTo || "",
dirDate: item.DirDate || item.checkIn || item.dateFrom || "",
dirWeekDay: item.DirWeekDay || "",
dirTimeFrom: item.DirTimeFrom || "",
dirTimeTo: item.DirTimeTo || "",
bakDate: item.BakDate || item.checkOut || item.dateTo || "",
bakWeekDay: item.BakWeekDay || "",
bakTimeFrom: item.BakTimeFrom || "",
bakTimeTo: item.BakTimeTo || "",
priceRub: priceRub,
priceRubNumber: Number.isFinite(priceRubNumber) ? priceRubNumber : 0,
priceTour: priceTour,
priceTourNumber: Number.isFinite(priceTourNumber) ? priceTourNumber : 0,
tourCurrency: tourCurrency,
link: item.bronUrl || item.packetUrl || item.url || "",
programLink: programLink,
catClaim: catClaimValue,
flightInCode: flightInCode,
flightOutCode: flightOutCode,
baggage: baggage,
hotelAvailabilityCode: hotelAvailabilityCode,
stopSale: isStopSale,
momentConfirm: isMomentConfirm
};
});
}
function applyPriceRangeFilter(items, priceFrom, priceTo, priceMode) {
var min = Number(priceFrom || 0);
var max = Number(priceTo || 0);
var hasMin = Number.isFinite(min) && min > 0;
var hasMax = Number.isFinite(max) && max > 0;
if (!hasMin && !hasMax) {
return items;
}
return items.filter(function (item) {
var value = priceMode === "rub" ? Number(item.priceRubNumber || 0) : Number(item.priceTourNumber || 0);
if (!Number.isFinite(value) || value <= 0) {
return false;
}
if (hasMin && value < min) {
return false;
}
if (hasMax && value > max) {
return false;
}
return true;
});
}
function stripHtml(value) {
return String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}
function hasHtmlTags(value) {
return /<[^>]+>/.test(String(value || ""));
}
function decodeAvailabilityCode(code) {
var normalized = String(code || "").trim().toUpperCase();
if (!normalized) {
return { text: "по запросу", level: "info" };
}
if (normalized === "Y" || normalized === "A" || normalized === "OK") {
return { text: "есть", level: "ok" };
}
if (normalized === "L" || normalized === "M") {
return { text: "мало мест", level: "warn" };
}
if (normalized === "N" || normalized === "X" || normalized === "S") {
return { text: "нет мест", level: "error" };
}
if (normalized === "R" || normalized === "Q" || normalized === "P") {
return { text: "по запросу", level: "info" };
}
return { text: "по запросу", level: "info" };
}
function decodeHotelAvailability(raw) {
var text = String(raw || "").trim().toUpperCase();
if (!text) {
return { text: "по запросу", level: "info" };
}
// SAMO often returns per-segment codes, e.g. "RRRR". Use the worst status.
if (text.indexOf("N") >= 0 || text.indexOf("X") >= 0 || text.indexOf("S") >= 0) {
return { text: "нет", level: "error" };
}
if (text.indexOf("L") >= 0 || text.indexOf("M") >= 0) {
return { text: "мало", level: "warn" };
}
if (text.indexOf("Y") >= 0 || text.indexOf("A") >= 0) {
return { text: "есть", level: "ok" };
}
return { text: "по запросу", level: "info" };
}
function normalizeWeekDayShort(value) {
var map = {
"1": "пн",
"2": "вт",
"3": "ср",
"4": "чт",
"5": "пт",
"6": "сб",
"7": "вс"
};
return map[String(value)] || "";
}
function formatTimeRange(from, to) {
var fromText = String(from || "").slice(0, 5);
var toText = String(to || "").slice(0, 5);
if (fromText && toText) {
return fromText + "-" + toText;
}
return fromText || toText || "";
}
function formatFlightCell(dateText, weekDayValue, timeFrom, timeTo) {
var dateLine = dateText || "-";
var weekDay = normalizeWeekDayShort(weekDayValue);
var timeLine = formatTimeRange(timeFrom, timeTo);
var secondLine = [weekDay, timeLine].filter(Boolean).join(" ");
if (!secondLine) {
return dateLine;
}
return dateLine + "" + secondLine + " ";
}
function getPriceMode() {
var checked = form.querySelector('input[name="ncr"]:checked');
return checked ? checked.value : "tour";
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function buildRouteLabel(routeText, context, alt) {
var explicit = String(routeText || "").trim();
if (explicit) {
return explicit;
}
var from = context && context.departureCity ? String(context.departureCity) : "";
var toParts = [context && context.city ? String(context.city) : "", context && context.country ? String(context.country) : ""].filter(Boolean);
var to = toParts.join(", ");
var codes = [(alt && alt.flightInCode) || "", (alt && alt.flightOutCode) || ""].filter(Boolean).join(" / ");
var base = [from, to].filter(Boolean).join(" - ");
if (base && codes) {
return base + " (" + codes + ")";
}
return base || codes || "маршрут уточняется";
}
function toNumberSafe(value) {
var num = Number(value);
return Number.isFinite(num) ? num : null;
}
function formatMinutesRu(minutes) {
var total = toNumberSafe(minutes);
if (total === null || total < 0) {
return "";
}
var h = Math.floor(total / 60);
var m = Math.floor(total % 60);
if (h && m) {
return h + " ч " + m + " м";
}
if (h) {
return h + " ч";
}
return m + " м";
}
function normalizeStopsLabel(stops, segments) {
var parsed = toNumberSafe(stops);
// Не использовать segments.length - 1: для RT два сегмента ≠ одна пересадка.
if (parsed === null && Array.isArray(segments) && segments.length > 0) {
parsed = countOfferStops(segments);
}
if (parsed === null) {
return "пересадки: уточняется";
}
if (parsed <= 0) {
return "без пересадок";
}
return parsed + " пересад.";
}
function parseSegmentsSafe(rawEncoded) {
if (!rawEncoded) {
return [];
}
try {
var decoded = decodeURIComponent(String(rawEncoded));
var parsed = JSON.parse(decoded);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
return [];
}
}
function hasBaggageOption(value) {
var txt = String(value || "").toLowerCase().trim();
if (!txt || txt === "-" || txt.indexOf("уточ") >= 0) {
return null;
}
if (txt.indexOf("нет") >= 0 || txt === "0pc" || txt === "0p" || txt === "no") {
return false;
}
return true;
}
function formatBaggageLabel(value) {
var txt = String(value || "").trim();
if (!txt || /^уточняется$/i.test(txt) || /^n\/?a$/i.test(txt) || txt === "-" || txt === "—") {
return "ручная кладь";
}
return txt;
}
function formatBaggagePrefLabel(value) {
if (value === "without") {
return "Только без багажа";
}
if (value === "all") {
return "Не важно";
}
return "Только с багажом";
}
function formatSegmentsText(segments) {
if (!Array.isArray(segments) || !segments.length) {
return "";
}
return segments.map(function (seg, idx) {
var route = [seg.from || "", seg.to || ""].filter(Boolean).join(" - ");
var carrier = [seg.carrier || "", seg.flightNumber || ""].filter(Boolean).join(" ");
var times = [seg.departure || "", seg.arrival || ""].filter(Boolean).join(" - ");
return (idx + 1) + ") " + [route, times, carrier].filter(Boolean).join(" | ");
}).join("; ");
}
function applyFlightCalcToContext(context, flightCalc, selected) {
var source = flightCalc || selected || null;
if (!source) {
return;
}
if (flightCalc) {
context.flightAvailable = !!flightCalc.available;
context.baggage = flightCalc.baggage || context.baggage || "";
context.route = flightCalc.route || context.route || "";
context.flightInCode = flightCalc.flightInCode || context.flightInCode || "";
context.flightOutCode = flightCalc.flightOutCode || context.flightOutCode || "";
context.flightDuration = flightCalc.durationText || context.flightDuration || "";
context.flightStops = flightCalc.stops || context.flightStops || "";
context.offerGuid = flightCalc.offerGuid || context.offerGuid || "";
context.flightBasePrice = flightCalc.basePrice != null ? String(flightCalc.basePrice) : (context.flightBasePrice || "");
context.flightDelta = flightCalc.deltaPrice != null && Number(flightCalc.deltaPrice) !== 0 ? String(flightCalc.deltaPrice) : "0";
context.flightAlternativesCount = flightCalc.alternativesCount != null ? String(flightCalc.alternativesCount) : "";
var segments = flightCalc.segments || [];
context.flightSegmentsJson = JSON.stringify(segments);
context.flightSegmentsText = formatSegmentsText(segments);
} else if (selected) {
context.route = buildRouteLabel(selected.route, context, selected);
context.flightInCode = selected.flightInCode || context.flightInCode || "";
context.flightOutCode = selected.flightOutCode || context.flightOutCode || "";
context.baggage = selected.baggage || context.baggage || "";
context.flightDuration = selected.durationText || formatMinutesRu(selected.durationMinutes) || context.flightDuration || "";
context.flightStops = normalizeStopsLabel(selected.stops, selected.segments) || context.flightStops || "";
context.offerGuid = selected.offer_guid || context.offerGuid || "";
context.flightBasePrice = selected.basePrice != null ? String(selected.basePrice) : context.flightBasePrice || "";
context.flightDelta = selected.deltaPrice != null && Number(selected.deltaPrice) !== 0 ? String(selected.deltaPrice) : "0";
var selectedSegments = Array.isArray(selected.segments) ? selected.segments : [];
context.flightSegmentsJson = JSON.stringify(selectedSegments);
context.flightSegmentsText = formatSegmentsText(selectedSegments);
}
// Пересобираем маршрут по ориентированным ногам (VVO→SVX / SVX→VVO),
// чтобы не показывать SVX-SVX и перепутанные направления.
try {
var segsForRoute = context.flightSegmentsJson ? JSON.parse(context.flightSegmentsJson) : [];
if (Array.isArray(segsForRoute) && segsForRoute.length) {
var oriented = splitSegmentsByDirection(segsForRoute, context);
var rebuilt = buildRouteFromLegs(oriented.outbound, oriented.inbound);
if (rebuilt) {
context.route = rebuilt;
}
var outDur = formatMinutesRu(sumSegmentsDurationMinutes(oriented.outbound));
var inDur = formatMinutesRu(sumSegmentsDurationMinutes(oriented.inbound));
if (outDur || inDur) {
context.flightDuration = [outDur ? "туда " + outDur : "", inDur ? "обратно " + inDur : ""].filter(Boolean).join(", ");
}
context.flightStops =
[
oriented.outbound.length ? "туда: " + normalizeStopsLabel(Math.max(oriented.outbound.length - 1, 0), oriented.outbound) : "",
oriented.inbound.length ? "обратно: " + normalizeStopsLabel(Math.max(oriented.inbound.length - 1, 0), oriented.inbound) : ""
]
.filter(Boolean)
.join(", ") || context.flightStops;
}
} catch (routeErr) {
// keep previous route
}
context.baggageLabel = formatBaggageLabel(context.baggage);
context.baggageRequired = "all";
context.baggageRequiredLabel = "Не важно";
}
function updateFlightChangeAvailability(context, alternatives, meta) {
meta = meta || {};
var alts = Array.isArray(alternatives) ? alternatives : [];
context._flightAlternatives = alts;
if (alts.length > 1) {
context.flightChangeAvailable = true;
context.flightChangeNote = "";
return;
}
context.flightChangeAvailable = false;
if (meta.loading) {
context.flightChangeNote = "Ищем варианты перелета…";
return;
}
if (meta.failed) {
var failMsg = meta.message ? String(meta.message).replace(/\s+/g, " ").trim() : "";
if (failMsg.length > 160) {
failMsg = failMsg.slice(0, 157) + "…";
}
context.flightChangeNote = failMsg
? ("Смена перелета недоступна: " + failMsg)
: "Смена перелета недоступна — не удалось получить варианты. Агент подберёт при заявке.";
return;
}
if (meta.ok) {
context.flightChangeNote = alts.length === 1
? "Других вариантов перелета сейчас нет"
: "Варианты для смены перелета не найдены";
return;
}
context.flightChangeNote = "Смена перелета будет доступна после уточнения рейса";
}
function renderFlightSummaryBlock(context) {
var canChangeFlight = !!context.flightChangeAvailable
|| (Array.isArray(context._flightAlternatives) && context._flightAlternatives.length > 1);
if (!context.route && !context.baggage && !context.flightDuration && !canChangeFlight && !context.flightChangeNote) {
return 'Данные перелета появятся после актуализации.
';
}
var segments = [];
try {
segments = context.flightSegmentsJson ? JSON.parse(context.flightSegmentsJson) : [];
} catch (e) {
segments = [];
}
var changeAction = canChangeFlight
? 'Сменить перелет '
: (context.flightChangeNote
? '' + escapeHtml(context.flightChangeNote) + "
"
: "");
return (
'' +
'
' +
'
Авиабилеты и багаж
' +
(changeAction ? '
' + changeAction + "
" : "") +
"
" +
'
' +
'
Маршрут: ' + escapeHtml(context.route || "-") + "
" +
(Number(context.flightDelta) !== 0
? '
Доплата за перелет: ' + escapeHtml(formatFlightDeltaLabel(context.flightDelta, context.tourCurrency || "")) + "
"
: "") +
"
" +
'
Багаж: ' +
escapeHtml(context.baggageLabel || formatBaggageLabel(context.baggage)) +
"
" +
(segments.length ? '
' + renderSegmentsStructured(segments, context) + "
" : "") +
"
"
);
}
function bindFlightChangeButton(body, context) {
if (!body) {
return;
}
var btn = body.querySelector(".etw-flight-summary-wrap .etw-flight-open-modal");
if (!btn) {
return;
}
btn.onclick = function () {
if (!Array.isArray(context._flightAlternatives) || context._flightAlternatives.length < 2) {
return;
}
openFlightAlternativesModal(context._flightAlternatives, context, body, context._rerunActualize);
};
}
function syncFlightSummaryUi(body, context) {
var holder = body.querySelector(".etw-flight-summary-wrap");
if (holder) {
holder.innerHTML = renderFlightSummaryBlock(context);
bindFlightChangeButton(body, context);
}
var routeNode = body.querySelector(".etw-meta-route");
if (routeNode) {
routeNode.textContent = context.route || "-";
}
var baggageNode = body.querySelector(".etw-meta-baggage");
if (baggageNode) {
baggageNode.textContent = context.baggageLabel || formatBaggageLabel(context.baggage) || "-";
}
var flightCodesNode = body.querySelector(".etw-meta-flight-codes");
if (flightCodesNode) {
flightCodesNode.textContent = (context.flightInCode || "-") + " / " + (context.flightOutCode || "-");
}
}
function formatMoney(amount, currency) {
var n = Number(amount);
if (!isFinite(n)) {
return "-";
}
var rounded = Math.round(n * 100) / 100;
return String(rounded) + (currency ? " " + currency : "");
}
function buildActualizeApiParams(context, item, options) {
options = options || {};
var offerGuid = "";
if (!options.skipOfferGuid) {
offerGuid = context.offerGuid || "";
} else if (options.offerGuidHint) {
offerGuid = String(options.offerGuidHint);
}
return {
resultId: item.resultId || "",
catClaim: item.catClaim || context.catClaim || "",
BAGGAGE_REQUIRED: "all",
WITH_INSURANCE: context.withInsurance ? "1" : "0",
WITH_VISA: context.withVisa ? "1" : "0",
OFFER_GUID: offerGuid,
TOWNFROMINC: String(ensureDepartureCitySelected() || ""),
STATEINC: String(countryNode.value || ""),
TOURTYPE: String(tourTypeNode.value || ""),
TOURINC: String(tourNode.value || ""),
CHECKIN_BEG: toIsoDate(dateFromNode.value),
CHECKIN_END: toIsoDate(dateToNode.value),
TOUR_CHECKIN: toIsoDate(item.checkIn || item.dateFrom || context.dateFrom || ""),
TOUR_CHECKOUT: toIsoDate(item.checkOut || item.dateTo || context.dateTo || ""),
NIGHTS_FROM: String(nightsFromNode.value || ""),
NIGHTS_TILL: String(nightsToNode.value || ""),
ADULT: String((adultsNode && adultsNode.value) || "2"),
CHILD: String(childsNode.value || "0"),
CHILD1AGE: child1AgeNode.disabled ? "" : child1AgeNode.value,
CHILD2AGE: child2AgeNode.disabled ? "" : child2AgeNode.value,
CHILD_AGES: context.childAges || "",
CURRENCY: String(item.currencyKey || "1"),
FREIGHT: hasFlightSeatsNode && hasFlightSeatsNode.checked ? "1" : ""
};
}
function applyOptionsCalcToContext(context, optionsCalc, selected) {
if (!optionsCalc || typeof optionsCalc !== "object") {
return;
}
context.insuranceAvailable = !!(optionsCalc.insurance && optionsCalc.insurance.available);
context.visaAvailable = !!(optionsCalc.visa && optionsCalc.visa.available);
context.insuranceReason = optionsCalc.insurance && optionsCalc.insurance.reason
? String(optionsCalc.insurance.reason)
: "";
context.visaReason = optionsCalc.visa && optionsCalc.visa.reason
? String(optionsCalc.visa.reason)
: "";
if (optionsCalc.insurance && !optionsCalc.insurance.available && context.withInsurance) {
context.withInsurance = false;
}
if (optionsCalc.visa && !optionsCalc.visa.available && context.withVisa) {
context.withVisa = false;
}
if (selected) {
var optTour = selected.priceTour !== undefined && selected.priceTour !== null && selected.priceTour !== ""
? selected.priceTour
: selected.price;
var optRub = selected.priceRub;
if (isPositiveMoney(optTour) && !looksLikeFlightOnlyPrice(optTour, context.searchPriceTour || context.priceTour)) {
context.priceTour = String(parseMoney(optTour));
}
if (isPositiveMoney(optRub) && !looksLikeFlightOnlyPrice(optRub, context.searchPriceRub || context.priceRub)) {
context.priceRub = String(Math.round(parseMoney(optRub)));
}
ensureFallbackTourPrices(context);
if (selected.tourCurrency) {
context.tourCurrency = selected.tourCurrency;
}
context.basePriceTour = selected.basePrice != null ? String(selected.basePrice) : (optionsCalc.basePrice != null ? String(optionsCalc.basePrice) : "");
context.insuranceAmount = selected.insuranceAmount != null ? String(selected.insuranceAmount) : (optionsCalc.insurance && optionsCalc.insurance.applied ? String(optionsCalc.insurance.amount) : "0");
context.visaAmount = selected.visaAmount != null ? String(selected.visaAmount) : (optionsCalc.visa && optionsCalc.visa.applied ? String(optionsCalc.visa.amount) : "0");
}
context.optionsCalcNote = formatOptionsCalcStatus(optionsCalc, context);
if (optionsCalc.warnings && optionsCalc.warnings.length) {
context.optionsCalcWarnings = optionsCalc.warnings.join(" ");
} else {
context.optionsCalcWarnings = "";
}
}
function serviceOptionUnavailableText(kind, reason) {
var isIns = kind === "insurance";
if (reason === "no_options") {
return isIns
? "Для этого направления нет доступных вариантов страховки"
: "Для этого направления нет доступных вариантов визы";
}
if (reason === "load_failed") {
return isIns
? "Не удалось рассчитать страховку автоматически — уточнит агент"
: "Не удалось рассчитать визу автоматически — уточнит агент";
}
if (reason === "no_price") {
return isIns
? "Страховка найдена, но цена для расчета недоступна"
: "Виза найдена, но цена для расчета недоступна";
}
return isIns
? "Страховка недоступна для выбранного направления"
: "Виза недоступна для выбранного направления";
}
function collectOptionsCalcHints(context) {
// Причины недоступности страховки/визы показываем только в зелёном блоке статуса ниже.
return [];
}
function formatOptionsCalcStatus(optionsCalc, context) {
if (!optionsCalc && !context) {
return "";
}
var parts = [];
var currency = optionsCalc && optionsCalc.currency ? optionsCalc.currency : "";
if (optionsCalc && optionsCalc.insurance && optionsCalc.insurance.applied) {
parts.push("страховка +" + formatMoney(optionsCalc.insurance.amount, currency));
}
if (optionsCalc && optionsCalc.visa && optionsCalc.visa.applied) {
parts.push("виза +" + formatMoney(optionsCalc.visa.amount, currency));
}
if (context && context.insuranceAvailable === false) {
parts.push(serviceOptionUnavailableText("insurance", context.insuranceReason));
} else if (optionsCalc && optionsCalc.insurance && optionsCalc.insurance.requested && !optionsCalc.insurance.available) {
parts.push(serviceOptionUnavailableText("insurance", optionsCalc.insurance.reason));
}
if (context && context.visaAvailable === false) {
parts.push(serviceOptionUnavailableText("visa", context.visaReason));
} else if (optionsCalc && optionsCalc.visa && optionsCalc.visa.requested && !optionsCalc.visa.available) {
parts.push(serviceOptionUnavailableText("visa", optionsCalc.visa.reason));
}
return parts.length ? parts.join("; ") : "";
}
function optionsCalcHintHtml() {
return '
';
}
function renderOptionsCalcControls(context) {
var insDisabled = context.insuranceAvailable === false;
var visaDisabled = context.visaAvailable === false;
var insHint = insDisabled ? serviceOptionUnavailableText("insurance", context.insuranceReason) : "";
var visaHint = visaDisabled ? serviceOptionUnavailableText("visa", context.visaReason) : "";
var insTitle = insHint ? ' title="' + escapeHtml(insHint) + '"' : "";
var visaTitle = visaHint ? ' title="' + escapeHtml(visaHint) + '"' : "";
return (
'' +
'" +
' " +
"Добавить страховку" +
" " +
'" +
' " +
"Добавить визу" +
" " +
"
" +
optionsCalcHintHtml()
);
}
function syncOptionsCalcUi(body, context) {
var insCb = body.querySelector(".etw-options-calc__insurance");
var visaCb = body.querySelector(".etw-options-calc__visa");
var noteNode = body.querySelector(".etw-options-calc__note");
if (insCb) {
insCb.checked = !!context.withInsurance;
insCb.disabled = context.insuranceAvailable === false;
var insLabel = insCb.closest(".etw-options-calc__item");
if (insLabel) {
insLabel.classList.toggle("is-disabled", context.insuranceAvailable === false);
insLabel.title = context.insuranceAvailable === false
? serviceOptionUnavailableText("insurance", context.insuranceReason)
: "";
}
}
if (visaCb) {
visaCb.checked = !!context.withVisa;
visaCb.disabled = context.visaAvailable === false;
var visaLabel = visaCb.closest(".etw-options-calc__item");
if (visaLabel) {
visaLabel.classList.toggle("is-disabled", context.visaAvailable === false);
visaLabel.title = context.visaAvailable === false
? serviceOptionUnavailableText("visa", context.visaReason)
: "";
}
}
if (noteNode) {
noteNode.hidden = true;
noteNode.innerHTML = "";
}
}
function bindOptionsCalcControls(body, context, rerunActualize) {
var insCb = body.querySelector(".etw-options-calc__insurance");
var visaCb = body.querySelector(".etw-options-calc__visa");
if (insCb) {
insCb.addEventListener("change", function () {
if (context.insuranceAvailable === false) {
insCb.checked = false;
return;
}
context.withInsurance = insCb.checked;
context.fareNote = context.withInsurance ? "Пересчитываем цену со страховкой…" : "Пересчитываем цену без страховки…";
refreshModalContext(body, context);
rerunActualize().catch(function () {});
});
}
if (visaCb) {
visaCb.addEventListener("change", function () {
if (context.visaAvailable === false) {
visaCb.checked = false;
return;
}
context.withVisa = visaCb.checked;
context.fareNote = context.withVisa ? "Пересчитываем цену с визой…" : "Пересчитываем цену без визы…";
refreshModalContext(body, context);
rerunActualize().catch(function () {});
});
}
}
function normalizePlaceToken(value) {
return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
}
var DEPARTURE_CITY_IATA = {
владивосток: "vvo",
екатеринбург: "svx",
ереван: "evn",
москва: "mow",
санктпетербург: "led",
"санкт-Петербург": "led",
новосибирск: "ovb",
красноярск: "kja",
иркутск: "ikt",
хабаровск: "khv",
казань: "kzn",
сочи: "aer",
минеральныеводы: "mrv",
"минеральные воды": "mrv",
самара: "kuf",
уфа: "ufa",
пермь: "pee",
челябинск: "cek",
омск: "oms",
ростовнадону: "rov",
"ростов-на-дону": "rov",
калининград: "kgd",
тюмень: "tjm",
ярославль: "iar",
нижнийновгород: "goj",
"нижний новгород": "goj"
};
function getDepartureMatchTokens(context) {
var tokens = [];
var name = normalizePlaceToken(context && context.departureCity ? context.departureCity : "");
if (name) {
tokens.push(name);
var compact = name.replace(/[-\s]/g, "");
if (DEPARTURE_CITY_IATA[name]) {
tokens.push(DEPARTURE_CITY_IATA[name]);
} else if (DEPARTURE_CITY_IATA[compact]) {
tokens.push(DEPARTURE_CITY_IATA[compact]);
}
}
if (depCityNode && depCityNode.value) {
var town = (fullTownFroms || []).find(function (item) {
return String(item.id) === String(depCityNode.value);
});
if (town) {
var townName = normalizePlaceToken(town.name || town.townName || "");
if (townName) {
tokens.push(townName);
var townCompact = townName.replace(/[-\s]/g, "");
if (DEPARTURE_CITY_IATA[townName]) {
tokens.push(DEPARTURE_CITY_IATA[townName]);
} else if (DEPARTURE_CITY_IATA[townCompact]) {
tokens.push(DEPARTURE_CITY_IATA[townCompact]);
}
}
if (town.iata || town.townIATA || town.alias) {
tokens.push(normalizePlaceToken(town.iata || town.townIATA || town.alias));
}
}
}
return tokens.filter(Boolean).filter(function (token, idx, arr) {
return arr.indexOf(token) === idx;
});
}
function placeMatchesTokens(place, tokens) {
var p = normalizePlaceToken(place);
if (!p || !tokens || !tokens.length) {
return false;
}
return tokens.some(function (token) {
if (!token) {
return false;
}
if (p === token) {
return true;
}
// IATA-код внутри "Koltsovo (SVX)" и т.п.
if (token.length === 3 && new RegExp("(?:^|[^a-z])" + token + "(?:[^a-z]|$)", "i").test(p)) {
return true;
}
return token.length > 3 && (p.indexOf(token) >= 0 || token.indexOf(p) >= 0);
});
}
function orientFlightLegs(outbound, inbound, context) {
var out = Array.isArray(outbound) ? outbound.slice() : [];
var inn = Array.isArray(inbound) ? inbound.slice() : [];
if (!out.length || !inn.length) {
return { outbound: out, inbound: inn };
}
var tokens = getDepartureMatchTokens(context);
if (!tokens.length) {
return { outbound: out, inbound: inn };
}
var outStartsHome = placeMatchesTokens(out[0].from, tokens);
var inStartsHome = placeMatchesTokens(inn[0].from, tokens);
var outEndsHome = placeMatchesTokens(out[out.length - 1].to, tokens);
var inEndsHome = placeMatchesTokens(inn[inn.length - 1].to, tokens);
// GDS иногда отдаёт route_index наоборот: «туда» = прилёт домой.
if ((!outStartsHome && inStartsHome) || (outEndsHome && !inEndsHome && !outStartsHome)) {
return { outbound: inn, inbound: out };
}
return { outbound: out, inbound: inn };
}
function splitSegmentsByDirection(segments, context) {
var list = Array.isArray(segments) ? segments : [];
if (!list.length) {
return { outbound: [], inbound: [] };
}
var hasRouteIndex = list.some(function (seg) {
return seg && (seg.routeIndex === 0 || seg.routeIndex === 1 || seg.routeIndex === "0" || seg.routeIndex === "1");
});
var outbound = [];
var inbound = [];
if (hasRouteIndex) {
outbound = list.filter(function (seg) {
return Number(seg.routeIndex) === 0;
});
inbound = list.filter(function (seg) {
return Number(seg.routeIndex) === 1;
});
} else {
var depTokens = getDepartureMatchTokens(context);
var splitIdx = -1;
if (depTokens.length) {
for (var i = 1; i < list.length; i += 1) {
if (placeMatchesTokens(list[i].from, depTokens) || placeMatchesTokens(list[i].to, depTokens)) {
splitIdx = i;
break;
}
}
}
if (splitIdx < 0 && list.length > 1) {
splitIdx = Math.ceil(list.length / 2);
}
if (splitIdx < 0) {
outbound = list;
inbound = [];
} else {
outbound = list.slice(0, splitIdx);
inbound = list.slice(splitIdx);
}
}
return orientFlightLegs(outbound, inbound, context);
}
function sumSegmentsDurationMinutes(segments) {
var list = Array.isArray(segments) ? segments : [];
var total = 0;
var matched = 0;
list.forEach(function (seg) {
var raw = String((seg && seg.duration) || "").trim();
if (!raw) {
return;
}
var hm = raw.match(/^(\d+)\s*[:ч]\s*(\d+)/i);
if (hm) {
total += Number(hm[1]) * 60 + Number(hm[2]);
matched += 1;
return;
}
var onlyMin = toNumberSafe(raw);
if (onlyMin !== null) {
total += onlyMin;
matched += 1;
}
});
return matched ? total : null;
}
function buildRouteFromLegs(outbound, inbound) {
var out = Array.isArray(outbound) ? outbound : [];
var inn = Array.isArray(inbound) ? inbound : [];
var outFrom = out[0] && out[0].from ? String(out[0].from) : "";
var outTo = out.length ? String(out[out.length - 1].to || "") : "";
var inFrom = inn[0] && inn[0].from ? String(inn[0].from) : "";
var inTo = inn.length ? String(inn[inn.length - 1].to || "") : "";
if (outFrom && outTo && inFrom && inTo) {
return outFrom + " - " + outTo + " / " + inFrom + " - " + inTo;
}
if (outFrom && outTo) {
return outFrom + " - " + outTo;
}
return "";
}
function collectLegBaggage(segments, fallback) {
var bags = [];
(Array.isArray(segments) ? segments : []).forEach(function (seg) {
var bag = String((seg && seg.baggage) || "").trim();
if (bag && bags.indexOf(bag) < 0) {
bags.push(bag);
}
});
if (bags.length) {
return bags.join(", ");
}
return String(fallback || "").trim();
}
function formatSegmentOneLine(seg) {
var routeLine = [seg.from || "", seg.to || ""].filter(Boolean).join(" → ");
var timeLine = [seg.departure || "", seg.arrival || ""].filter(Boolean).join(" – ");
var carrier = [seg.carrier || "", seg.flightNumber || ""].filter(Boolean).join(" ");
var bag = String(seg.baggage || "").trim();
return [routeLine || "Маршрут не указан", timeLine, seg.duration || "", carrier, bag ? "багаж: " + bag : ""]
.filter(Boolean)
.join(" · ");
}
function renderSegmentsDirection(title, segments, context, tone) {
if (!Array.isArray(segments) || !segments.length) {
return "";
}
var durationText = formatMinutesRu(sumSegmentsDurationMinutes(segments)) || "уточняется";
var stopsText = normalizeStopsLabel(Math.max(segments.length - 1, 0), segments);
var bagText = formatBaggageLabel(collectLegBaggage(segments, context && context.baggage));
return (
'' +
'
' + escapeHtml(title) + "
" +
'
В пути: ' +
escapeHtml(durationText) +
" | " +
escapeHtml(stopsText) +
" | Багаж: " +
escapeHtml(bagText) +
"
" +
'
' +
segments.map(function (seg, idx) {
return (
'
' +
escapeHtml(formatSegmentOneLine(seg)) +
"
"
);
}).join("") +
"
" +
"
"
);
}
function renderSegmentsStructured(segments, context) {
var split = splitSegmentsByDirection(segments, context);
return (
'' +
renderSegmentsDirection("Туда", split.outbound, context, "out") +
renderSegmentsDirection("Обратно", split.inbound, context, "back") +
"
"
);
}
function getSelectedLabels(selectNode) {
return Array.prototype.slice
.call((selectNode && selectNode.options) || [])
.filter(function (option) {
return option.selected && option.value !== "";
})
.map(function (option) {
return option.textContent || "";
});
}
function buildRequestContext(item) {
var selectedHotels = getSelectedLabels(hotelSelectNode);
var selectedTowns = getSelectedLabels(tourCityNode);
var selectedStars = getSelectedLabels(hotelStarsNode);
var selectedMeals = getSelectedLabels(mealNode);
var selectedTourType = tourTypeNode.options[tourTypeNode.selectedIndex] ? tourTypeNode.options[tourTypeNode.selectedIndex].text : "";
var selectedTour = tourNode.options[tourNode.selectedIndex] ? tourNode.options[tourNode.selectedIndex].text : "";
var priceMode = getPriceMode();
return {
hotel: item.hotel || "",
star: item.star || "",
country: item.country || (countryNode.options[countryNode.selectedIndex] ? countryNode.options[countryNode.selectedIndex].text : ""),
city: item.city || selectedTowns.join(", "),
departureCity: item.departureCity || (depCityNode.options[depCityNode.selectedIndex] ? depCityNode.options[depCityNode.selectedIndex].text : ""),
dateFrom: item.dateFrom || dateFromNode.value || "",
dateTo: item.dateTo || dateToNode.value || "",
nights: item.nights || "",
adults: adultsNode ? adultsNode.value : "",
childs: childsNode.value || "",
child1Age: child1AgeNode.disabled ? "" : child1AgeNode.value,
child2Age: child2AgeNode.disabled ? "" : child2AgeNode.value,
childAges: [child1AgeNode.disabled ? "" : child1AgeNode.value, child2AgeNode.disabled ? "" : child2AgeNode.value].filter(Boolean).join(","),
meal: item.meal || selectedMeals.join(", "),
room: item.room || "",
tourType: selectedTourType,
tour: item.tour || selectedTour,
hotelsFilter: selectedHotels.join(", "),
townsFilter: selectedTowns.join(", "),
starsFilter: selectedStars.join(", "),
priceMode: priceMode,
priceRub: item.priceRub || "",
priceTour: item.priceTour || "",
searchPriceRub: item.priceRub || "",
searchPriceTour: item.priceTour || "",
priceExact: false,
tourCurrency: item.tourCurrency || "",
priceFrom: priceFromNode ? priceFromNode.value : "",
priceTo: priceToNode ? priceToNode.value : "",
childInBed: childInBedNode && childInBedNode.checked ? "1" : "0",
hasFlightSeats: hasFlightSeatsNode && hasFlightSeatsNode.checked ? "1" : "0",
noStopSale: noStopSaleNode && noStopSaleNode.checked ? "1" : "0",
momentConfirm: momentConfirmNode && momentConfirmNode.checked ? "1" : "0",
agentEmail: agentEmail,
host: statHost,
widgetName: widgetName,
widgetVersion: widgetVersion,
resultId: item.resultId || "",
catClaim: item.catClaim || "",
hotelUrl: item.hotelUrl || "",
tourLink: item.link || "",
flightInCode: item.flightInCode || "",
flightOutCode: item.flightOutCode || "",
baggage: item.baggage || "",
baggageRequired: "all",
offerGuid: "",
flightDuration: "",
flightStops: "",
flightSegmentsJson: "[]",
flightSegmentsText: "",
flightBasePrice: "",
flightDelta: "0",
flightAlternativesCount: "",
baggageLabel: "",
baggageRequiredLabel: "Не важно",
flightAvailable: null,
withInsurance: false,
withVisa: false,
insuranceAvailable: null,
insuranceReason: "",
visaReason: "",
visaAvailable: null,
basePriceTour: "",
insuranceAmount: "0",
visaAmount: "0",
optionsCalcNote: "",
optionsCalcWarnings: "",
flightChangeAvailable: false,
flightChangeNote: "Смена перелета будет доступна после уточнения рейса",
route: item.route || "",
fareNote: "Выполняется актуализация цены."
};
}
function renderPriceBreakdown(context) {
var currency = context.tourCurrency ? " " + context.tourCurrency : "";
var parts = [];
// Доплата за перелет показывается в блоке авиабилетов — здесь только опции к цене.
if (context.basePriceTour && (Number(context.insuranceAmount) > 0 || Number(context.visaAmount) > 0)) {
parts.push('Базовая цена: ' + escapeHtml(context.basePriceTour + currency) + "
");
}
if (Number(context.insuranceAmount) > 0) {
parts.push('Страховка: +' + escapeHtml(context.insuranceAmount + currency) + "
");
}
if (Number(context.visaAmount) > 0) {
parts.push('Виза: +' + escapeHtml(context.visaAmount + currency) + "
");
}
return parts.length
? '' + parts.join("") + "
"
: '
';
}
function renderRequestMeta(context) {
var childs = Number(context.childs || 0);
var tourists =
escapeHtml(String(context.adults || "0")) +
" взр." +
(childs > 0
? ", " +
escapeHtml(String(context.childs)) +
" дет." +
(context.childAges ? " (" + escapeHtml(context.childAges) + " лет)" : "")
: "");
var placeBits = [context.country, context.city].filter(Boolean);
var rows = [];
if (placeBits.length) {
rows.push('Куда: ' + escapeHtml(placeBits.join(", ")) + "
");
}
if (context.departureCity) {
rows.push('Откуда: ' + escapeHtml(context.departureCity) + "
");
}
rows.push(
'Даты: ' +
escapeHtml((context.dateFrom || "-") + " — " + (context.dateTo || "-")) +
(context.nights ? " · " + escapeHtml(String(context.nights)) + " нч." : "") +
"
"
);
rows.push('Туристы: ' + tourists + "
");
if (context.meal) {
rows.push('Питание: ' + escapeHtml(context.meal) + "
");
}
if (context.room) {
rows.push('Номер: ' + escapeHtml(context.room) + "
");
}
if (context.tour && context.tour !== context.hotel) {
rows.push('Программа: ' + escapeHtml(context.tour) + "
");
}
return (
'' +
rows.join("") +
renderPriceBreakdown(context) +
"
"
);
}
function parseMoney(value) {
if (value === undefined || value === null || value === "") {
return null;
}
var n = Number(String(value).replace(/\s/g, "").replace(",", ".").replace(/[^\d.-]/g, ""));
return Number.isFinite(n) ? n : null;
}
function isPositiveMoney(value) {
var n = parseMoney(value);
return n !== null && n > 0;
}
/** GDS часто отдаёт доплату за авиа (15–500), а не цену пакета. */
function looksLikeFlightOnlyPrice(candidate, baseline) {
var c = parseMoney(candidate);
var b = parseMoney(baseline);
if (c === null || c <= 0) {
return true;
}
if (b === null || b <= 0) {
return false;
}
return c < b * 0.35;
}
function formatMoneyWithCurrency(amount, currency) {
var n = parseMoney(amount);
if (n === null) {
return "-";
}
var rounded = Math.round(n * 100) / 100;
return String(rounded) + (currency ? " " + currency : "");
}
function formatFlightDeltaLabel(delta, currency) {
var n = parseMoney(delta);
if (n === null) {
n = 0;
}
var sign = n > 0 ? "+" : (n < 0 ? "" : "+");
return sign + formatMoneyWithCurrency(n, currency);
}
function packageBaseTourPrice(context) {
return parseMoney(context.searchPriceTour) || parseMoney(context.priceTour) || 0;
}
/**
* Цена пакета из поиска + доплата за выбранный GDS-перелет.
* Сам GDS часто отдаёт только авиатариф — им нельзя затирать цену тура.
*/
function applyFlightDeltaToTourPrice(context, delta) {
var base = packageBaseTourPrice(context);
var d = parseMoney(delta);
if (d === null) {
d = 0;
}
context.flightDelta = String(d);
if (base > 0) {
context.priceTour = String(Math.round((base + d) * 100) / 100);
context.priceExact = true;
}
var baseRub = parseMoney(context.searchPriceRub);
if (baseRub !== null && baseRub > 0 && (!context.tourCurrency || /rub|руб/i.test(String(context.tourCurrency)))) {
context.priceRub = String(Math.round(baseRub + d));
}
}
function ensureFallbackTourPrices(context) {
if (!isPositiveMoney(context.priceTour) && isPositiveMoney(context.searchPriceTour)) {
context.priceTour = String(parseMoney(context.searchPriceTour));
}
if (!isPositiveMoney(context.priceRub) && isPositiveMoney(context.searchPriceRub)) {
context.priceRub = String(Math.round(parseMoney(context.searchPriceRub)));
}
}
function formatModalPriceText(context) {
ensureFallbackTourPrices(context);
var amount = "";
var currency = "";
if (context.priceMode === "rub") {
amount = isPositiveMoney(context.priceRub)
? String(Math.round(parseMoney(context.priceRub)))
: "";
currency = "RUB";
} else {
amount = isPositiveMoney(context.priceTour) ? String(parseMoney(context.priceTour)) : "";
currency = context.tourCurrency || "";
}
if (!amount) {
return "Цена уточняется";
}
return amount + (currency ? " " + currency : "");
}
function syncModalPrice(context) {
var modal = document.getElementById("etwTourModal");
if (!modal) {
return;
}
var valueNode = modal.querySelector(".etw-tour-summary .etw-modal-price__value");
if (valueNode) {
valueNode.textContent = formatModalPriceText(context);
valueNode.classList.toggle("is-exact", !!context.priceExact);
valueNode.classList.toggle("is-from", !context.priceExact);
}
}
/**
* Пишем цену тура только если сумма валидна и не похожа на тариф авиа.
* exact=true — после успешной актуализации (точная), иначе «от».
*/
function applyTourDisplayPrice(context, tourPrice, rubPrice, exact) {
var baseline = context.searchPriceTour || context.priceTour;
if (isPositiveMoney(tourPrice) && !looksLikeFlightOnlyPrice(tourPrice, baseline)) {
context.priceTour = String(parseMoney(tourPrice));
}
if (isPositiveMoney(rubPrice) && !looksLikeFlightOnlyPrice(rubPrice, context.searchPriceRub || rubPrice)) {
context.priceRub = String(Math.round(parseMoney(rubPrice)));
}
ensureFallbackTourPrices(context);
var display = context.priceMode === "rub" ? context.priceRub : context.priceTour;
context.priceExact = !!exact && isPositiveMoney(display);
}
function renderHiddenInputs(context) {
return Object.keys(context)
.map(function (key) {
return ' ';
})
.join("");
}
function renderEmpty() {
tableContainer.innerHTML =
'К сожалению, мы не смогли найти ни одного тура по вашему запросу :( Попробуйте изменить условия поиска.
';
}
function renderResults(items, pager) {
if (!items.length) {
renderEmpty();
return;
}
pager = pager || lastSearchPager || { current: currentSearchPage || 1, total: 1 };
lastSearchPager = pager;
currentSearchPage = pager.current || currentSearchPage || 1;
renderResultMap = {};
var priceMode = getPriceMode();
var rows = items
.map(function (item, idx) {
var resultKey = "row_" + idx;
renderResultMap[resultKey] = item;
var departCell = formatFlightCell(item.dirDate || item.dateFrom, item.dirWeekDay, item.dirTimeFrom, item.dirTimeTo);
var returnCell = formatFlightCell(item.bakDate || item.dateTo, item.bakWeekDay, item.bakTimeFrom, item.bakTimeTo);
var priceLabel = "-";
if (priceMode === "rub") {
priceLabel = item.priceRub ? String(item.priceRub) + " RUB" : "-";
} else if (item.priceTour) {
// Price in operator/tour currency.
var priceTourText = String(item.priceTour);
if (item.tourCurrency && priceTourText.indexOf(item.tourCurrency) === -1) {
priceTourText += " " + item.tourCurrency;
}
priceLabel = priceTourText;
}
var flightIn = decodeAvailabilityCode(item.flightInCode);
var flightOut = decodeAvailabilityCode(item.flightOutCode);
var hotel = decodeHotelAvailability(item.hotelAvailabilityCode);
var marks = [];
var flightMarks =
'' +
'««« ' +
'»»» ' +
" ";
marks.push('рейс: ' + flightMarks + " ");
marks.push('отель: ' + hotel.text + " ");
if (item.momentConfirm) {
marks.push('мгн. подтверждение ');
}
if (item.stopSale) {
marks.push('stop-sale ');
}
var marksHtml = marks.join(" ");
var tourCell = item.tour || "-";
if (item.programLink && tourCell !== "-") {
tourCell =
'' +
tourCell +
" ";
}
if (String(stripHtml(item.spoRaw)).trim() !== "") {
var spoClass = hasHtmlTags(item.spoRaw) ? "etw-mark_spo_html" : "etw-mark_spo_plain";
tourCell += '' + String(item.spoRaw) + "
";
}
var hotelTitle = item.hotel || "-";
if (item.star) {
hotelTitle += " " + item.star;
}
var hotelCell = hotelTitle;
if (item.hotelUrl) {
hotelCell =
'' +
hotelTitle +
" ";
}
var priceButton =
'' +
priceLabel +
" ";
return (
"" +
"" +
departCell +
" " +
"" +
returnCell +
" " +
"" +
(item.nights || "-") +
" " +
"" +
(item.city || "-") +
" " +
"" +
hotelCell +
" " +
"" +
(item.room || "-") +
" " +
"" +
(item.meal || "-") +
" " +
"" +
tourCell +
" " +
"" +
marksHtml +
" " +
"" +
priceButton +
" " +
" "
);
})
.join("");
tableContainer.innerHTML =
'Вылет Возврат Ночей Город/курорт Гостиница Номер Питание Название тура Отметки Цена ' +
rows +
"
" +
renderSearchPager(pager);
}
async function fetchSearchResults(options) {
options = options || {};
var params = buildSearchParamsFromForm();
if (options.pricePage) {
params.PRICEPAGE = String(options.pricePage);
currentSearchPage = Number(options.pricePage) || 1;
}
var payload = await fetchJSON(buildApiUrl("search", params));
var list = normalizePrices(payload);
list = applyPriceRangeFilter(
list,
(priceFromNode && priceFromNode.value) || "",
(priceToNode && priceToNode.value) || "",
getPriceMode()
);
var pager = extractSearchPager(payload);
lastSearchPager = pager;
currentSearchPage = pager.current || currentSearchPage || 1;
return { items: list, pager: pager, raw: payload };
}
function buildShareRestorePageOrder(startPage, totalPages) {
var order = [];
var seen = {};
var start = Number(startPage) || 1;
var total = Number(totalPages) || 1;
if (start >= 1 && start <= total) {
order.push(start);
seen[start] = true;
}
for (var page = 1; page <= total; page += 1) {
if (!seen[page]) {
order.push(page);
}
}
return order;
}
async function findTourAcrossSearchPages(tourCriteria, searchParams) {
var preferredPage = Number((tourCriteria && tourCriteria.pricePage) || (searchParams && searchParams.PRICEPAGE) || 1) || 1;
var totalPages = 1;
var totalSeen = 0;
var pagesSearched = 0;
var lastResult = null;
var pageOrder = [preferredPage];
var page = 0;
while (page < pageOrder.length && page < 100) {
var pageNum = pageOrder[page];
if (pageNum < 1) {
page += 1;
continue;
}
var result = await fetchSearchResults({ pricePage: pageNum, skipStat: true });
lastResult = result;
totalPages = Math.max(totalPages, result.pager.total || 1);
pageOrder = buildShareRestorePageOrder(preferredPage, totalPages);
pagesSearched += 1;
totalSeen += result.items.length;
var matched = findTourInSearchResults(result.items, tourCriteria);
if (matched) {
return {
matched: matched,
result: result,
page: pageNum,
totalSeen: totalSeen,
pagesSearched: pagesSearched
};
}
page += 1;
}
return {
matched: null,
result: lastResult,
page: preferredPage,
totalSeen: totalSeen,
pagesSearched: pagesSearched
};
}
async function loadSearchPage(page) {
var nextPage = Number(page) || 1;
if (nextPage < 1) {
nextPage = 1;
}
currentSearchPage = nextPage;
searchButton.disabled = true;
setLoading(true, "40%");
try {
await executeSearch();
setLoading(true, "100%");
} catch (error) {
tableContainer.innerHTML = 'Ошибка загрузки данных: ' + error.message + "
";
} finally {
setTimeout(function () {
setLoading(false, "100%");
}, 300);
searchButton.disabled = false;
}
}
function ensureHotelModal() {
var existing = document.getElementById("etwHotelModal");
if (existing) {
return existing;
}
var modal = document.createElement("div");
modal.id = "etwHotelModal";
modal.className = "etw-hotel-modal";
modal.innerHTML =
'
' +
'";
document.body.appendChild(modal);
modal.querySelector(".etw-hotel-modal__backdrop").addEventListener("click", function () {
modal.classList.remove("is-open");
});
modal.querySelector(".etw-hotel-modal__close").addEventListener("click", function () {
modal.classList.remove("is-open");
});
return modal;
}
function renderHotelModalContent(content) {
var images = Array.isArray(content.images) ? content.images : [];
var tabs = content.tabs || {};
var tabKeys = ["tabs-place", "tabs-desc", "tabs-rooms", "tabs-service"];
var tabTitles = {
"tabs-place": "Расположение",
"tabs-desc": "Описание",
"tabs-rooms": "Номера",
"tabs-service": "Услуги"
};
var gallery = "";
if (images.length) {
var firstImage = images[0];
var thumbs = images
.slice(0, 16)
.map(function (src) {
return ' ';
})
.join("");
gallery =
'' +
'
' +
'
' +
thumbs +
"
" +
"
";
}
var nav = tabKeys
.filter(function (key) {
return tabs[key];
})
.map(function (key, index) {
return (
'' +
tabTitles[key] +
" "
);
})
.join("");
var panels = tabKeys
.filter(function (key) {
return tabs[key];
})
.map(function (key, index) {
return (
'' +
tabs[key] +
"
"
);
})
.join("");
return (
gallery +
'' +
'
' +
nav +
"
" +
'
' +
panels +
"
" +
"
"
);
}
async function openHotelModal(hotelUrl, hotelTitle) {
var modal = ensureHotelModal();
var bodyNode = modal.querySelector(".etw-hotel-modal__body");
var titleNode = modal.querySelector(".etw-hotel-modal__title");
titleNode.textContent = hotelTitle || "Карточка отеля";
modal.classList.add("is-open");
bodyNode.innerHTML = 'Загрузка...
';
try {
var payload = await fetchJSON(buildApiUrl("hotelcontent", { url: hotelUrl }));
var data = payload.data && payload.data.content ? payload.data.content : {};
bodyNode.innerHTML = renderHotelModalContent(data);
var mainImg = bodyNode.querySelector(".etw-hotel-main img");
bodyNode.querySelectorAll(".etw-hotel-thumb").forEach(function (btn) {
btn.addEventListener("click", function () {
var src = btn.getAttribute("data-src");
if (mainImg && src) {
mainImg.src = src;
}
});
});
bodyNode.querySelectorAll(".etw-hotel-tab").forEach(function (tabBtn) {
tabBtn.addEventListener("click", function () {
var tabKey = tabBtn.getAttribute("data-tab");
bodyNode.querySelectorAll(".etw-hotel-tab").forEach(function (btn) {
btn.classList.toggle("is-active", btn === tabBtn);
});
bodyNode.querySelectorAll(".etw-hotel-panel").forEach(function (panel) {
panel.classList.toggle("is-active", panel.getAttribute("data-panel") === tabKey);
});
});
});
} catch (error) {
bodyNode.innerHTML = 'Не удалось загрузить карточку отеля.
';
}
}
function splitCsv(value) {
return String(value || "")
.split(",")
.map(function (part) {
return String(part || "").trim();
})
.filter(Boolean);
}
function getShareHashCode() {
var hash = String(window.location.hash || "").replace(/^#/, "").trim();
if (!hash) {
return "";
}
// SAMO OnlineLink: 32 символа; legacy file-share: 10 символов.
if ((hash.length === 32 || hash.length === 10) && /^[a-zA-Z0-9]+$/.test(hash)) {
return hash;
}
return "";
}
function clearShareHash() {
if (!window.history || !window.history.replaceState) {
return;
}
var path = (window.location.pathname || "") + (window.location.search || "");
window.history.replaceState(null, "", path);
}
function getSharePagePath() {
return (window.location.pathname || "/") + (window.location.search || "");
}
function buildSearchParamsFromForm() {
var advancedOpen = isAdvancedFiltersOpen();
return {
TOWNFROMINC: ensureDepartureCitySelected(),
STATEINC: countryNode.value,
TOURTYPEINC: tourTypeNode.value || "",
TOURTYPE: tourTypeNode.value || "",
TOURINC: tourNode.value,
TOWNS: advancedOpen ? getSelectedValues(tourCityNode).join(",") : "",
STARS: advancedOpen ? getSelectedValues(hotelStarsNode).join(",") : "",
HOTELS: advancedOpen ? getSelectedValues(hotelSelectNode).join(",") : "",
CHECKIN_BEG: toIsoDate(dateFromNode.value),
CHECKIN_END: toIsoDate(dateToNode.value),
NIGHTS_FROM: nightsFromNode.value,
NIGHTS_TILL: nightsToNode.value,
ADULT: (adultsNode && adultsNode.value) || "2",
CHILD: childsNode.value,
CHILD1AGE: child1AgeNode.disabled ? "" : child1AgeNode.value,
CHILD2AGE: child2AgeNode.disabled ? "" : child2AgeNode.value,
CHILD_AGES: (function () {
var ages = [];
if (!child1AgeNode.disabled) {
ages.push(child1AgeNode.value);
}
if (!child2AgeNode.disabled) {
ages.push(child2AgeNode.value);
}
return ages.join(",");
})(),
MEALS: advancedOpen ? getSelectedValues(mealNode).join(",") : "",
COSTMIN: advancedOpen && priceFromNode ? priceFromNode.value || "" : "",
COSTMAX: advancedOpen && priceToNode ? priceToNode.value || "" : "",
CHILD_IN_BED: advancedOpen && childInBedNode && childInBedNode.checked ? 1 : "",
COMFORTABLE_SEATS: advancedOpen && hasFlightSeatsNode && hasFlightSeatsNode.checked ? 1 : "",
FILTER: advancedOpen && noStopSaleNode && noStopSaleNode.checked ? 1 : "",
MOMENT_CONFIRM: advancedOpen && momentConfirmNode && momentConfirmNode.checked ? 1 : "",
NCR: getPriceMode() === "rub" ? "true" : "false",
PRICEPAGE: String(currentSearchPage || 1),
limit: 100
};
}
function extractSearchPager(payload) {
var root = payload && payload.data ? payload.data : {};
var pager = root.result && root.result.pager ? root.result.pager : null;
var current = pager && pager.current != null ? Number(pager.current) : currentSearchPage || 1;
var total = pager && pager.total != null ? Number(pager.total) : 1;
if (!Number.isFinite(current) || current < 1) {
current = 1;
}
if (!Number.isFinite(total) || total < 1) {
total = 1;
}
return { current: current, total: total };
}
function renderSearchPager(pager) {
pager = pager || { current: 1, total: 1 };
if (!pager.total || pager.total <= 1) {
return "";
}
var html = '";
return html;
}
function buildSharePayloadData(context, item) {
return {
v: 1,
search: buildSearchParamsFromForm(),
tour: {
resultId: item.resultId || context.resultId || "",
catClaim: item.catClaim || context.catClaim || "",
hotel: item.hotel || context.hotel || "",
star: item.star || context.star || "",
city: item.city || context.city || "",
country: item.country || context.country || "",
dateFrom: item.dateFrom || context.dateFrom || "",
nights: item.nights || context.nights || "",
meal: item.meal || context.meal || "",
room: item.room || context.room || "",
tour: item.tour || context.tour || "",
flightInCode: context.flightInCode || item.flightInCode || "",
flightOutCode: context.flightOutCode || item.flightOutCode || "",
baggage: context.baggage || item.baggage || "",
pricePage: currentSearchPage || 1
},
actualize: {
baggageRequired: "all",
baggageRequiredLabel: "Не важно",
withInsurance: !!context.withInsurance,
withVisa: !!context.withVisa,
offerGuid: context.offerGuid || ""
}
};
}
function parseSharePayload(raw) {
var text = String(raw || "").trim();
if (!text) {
return null;
}
if (text.charAt(0) === "{") {
try {
var parsed = JSON.parse(text);
return parsed && typeof parsed === "object" ? parsed : null;
} catch (e) {
return null;
}
}
try {
var urlObj = new URL(text, window.location.href);
var search = {};
urlObj.searchParams.forEach(function (value, key) {
search[key] = value;
});
return { v: 1, search: search, tour: {}, actualize: {} };
} catch (e2) {
return null;
}
}
async function fetchShareResolve(code) {
var response = await fetch(buildApiUrl("share_resolve", { code: code }));
var text = await response.text();
var payload = null;
try {
payload = JSON.parse(text);
} catch (err) {
throw new Error("API вернул не JSON");
}
return {
httpStatus: response.status,
ok: !!(payload && payload.ok),
data: payload && payload.data ? payload.data : null,
message: (payload && payload.message) || "",
error: (payload && payload.error) || (payload && payload.data && payload.data.error) || ""
};
}
async function createShareLinkRequest(payloadText) {
var body = new URLSearchParams();
body.set("payload", payloadText);
body.set("host", statHost);
body.set("pagePath", getSharePagePath());
var response = await fetch(buildApiUrl("share", {}), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body: body.toString()
});
var text = await response.text();
var payload = null;
try {
payload = JSON.parse(text);
} catch (err) {
throw new Error("API вернул не JSON");
}
if (!response.ok || !payload || payload.ok === false) {
throw new Error((payload && payload.message) || "Не удалось создать ссылку.");
}
return payload.data || {};
}
function setMultiSelectValues(selectNode, values) {
var map = {};
(values || []).forEach(function (value) {
map[String(value)] = true;
});
Array.prototype.forEach.call(selectNode.options || [], function (option) {
option.selected = !!map[option.value];
});
updateFormstoneSelect(selectNode, true);
}
function applySearchParamsToForm(search) {
search = search || {};
if (search.TOWNFROMINC) {
depCityNode.value = String(search.TOWNFROMINC);
var town = fullTownFroms.find(function (item) {
return String(item.id) === String(search.TOWNFROMINC);
});
if (town && town.stateFromKey) {
countryFromNode.value = String(town.stateFromKey);
filterTownFromsByCountryFrom();
depCityNode.value = String(search.TOWNFROMINC);
syncDepartureFieldPresentation();
}
}
if (search.STATEINC) {
countryNode.value = String(search.STATEINC);
}
if (search.TOURTYPEINC || search.TOURTYPE) {
tourTypeNode.value = String(search.TOURTYPEINC || search.TOURTYPE);
}
if (search.TOURINC) {
tourNode.value = String(search.TOURINC);
}
if (search.CHECKIN_BEG) {
dateFromNode.value = String(search.CHECKIN_BEG);
}
if (search.CHECKIN_END) {
dateToNode.value = String(search.CHECKIN_END);
}
if (search.NIGHTS_FROM) {
nightsFromNode.value = String(search.NIGHTS_FROM);
}
if (search.NIGHTS_TILL) {
nightsToNode.value = String(search.NIGHTS_TILL);
}
if (search.ADULT && adultsNode) {
adultsNode.value = String(search.ADULT);
}
if (search.CHILD !== undefined && search.CHILD !== "") {
childsNode.value = String(search.CHILD);
}
updateChildAgeState();
if (search.CHILD1AGE !== undefined && search.CHILD1AGE !== "" && !child1AgeNode.disabled) {
child1AgeNode.value = String(search.CHILD1AGE);
}
if (search.CHILD2AGE !== undefined && search.CHILD2AGE !== "" && !child2AgeNode.disabled) {
child2AgeNode.value = String(search.CHILD2AGE);
}
if (priceFromNode && search.COSTMIN) {
priceFromNode.value = String(search.COSTMIN);
}
if (priceToNode && search.COSTMAX) {
priceToNode.value = String(search.COSTMAX);
}
if (childInBedNode) {
childInBedNode.checked = String(search.CHILD_IN_BED) === "1";
}
if (hasFlightSeatsNode) {
hasFlightSeatsNode.checked = String(search.COMFORTABLE_SEATS || search.FREIGHT) === "1";
}
if (noStopSaleNode) {
noStopSaleNode.checked = String(search.FILTER) === "1";
}
if (momentConfirmNode) {
momentConfirmNode.checked = String(search.MOMENT_CONFIRM) === "1";
}
var ncr = String(search.NCR || "").toLowerCase();
if (ncr === "true" || ncr === "1") {
var rubNode = form.querySelector('input[name="ncr"][value="rub"]');
if (rubNode) {
rubNode.checked = true;
}
} else if (ncr === "false" || ncr === "0") {
var tourNodeRadio = form.querySelector('input[name="ncr"][value="tour"]');
if (tourNodeRadio) {
tourNodeRadio.checked = true;
}
}
pendingShareMultiSelect = {
TOWNS: splitCsv(search.TOWNS),
STARS: splitCsv(search.STARS),
HOTELS: splitCsv(search.HOTELS),
MEALS: splitCsv(search.MEALS)
};
var hasAdvancedShare =
pendingShareMultiSelect.TOWNS.length > 0 ||
pendingShareMultiSelect.STARS.length > 0 ||
pendingShareMultiSelect.HOTELS.length > 0 ||
pendingShareMultiSelect.MEALS.length > 0 ||
!!String(search.COSTMIN || "") ||
!!String(search.COSTMAX || "") ||
String(search.CHILD_IN_BED) === "1" ||
String(search.COMFORTABLE_SEATS || search.FREIGHT) === "1" ||
String(search.FILTER) === "1" ||
String(search.MOMENT_CONFIRM) === "1";
if (hasAdvancedShare) {
setAdvancedFiltersVisible(true);
}
updateFormstoneSelect(countryFromNode, true);
updateFormstoneSelect(depCityNode, true);
updateFormstoneSelect(countryNode, true);
updateFormstoneSelect(tourTypeNode, true);
updateFormstoneSelect(tourNode, true);
updateFormstoneSelect(nightsFromNode, true);
updateFormstoneSelect(nightsToNode, true);
updateFormstoneSelect(child1AgeNode, true);
updateFormstoneSelect(child2AgeNode, true);
refreshAllFormstoneSelects(true);
}
function applyPendingShareMultiSelects() {
if (!pendingShareMultiSelect) {
return [];
}
var hotels = pendingShareMultiSelect.HOTELS.slice();
if (pendingShareMultiSelect.TOWNS.length) {
setMultiSelectValues(tourCityNode, pendingShareMultiSelect.TOWNS);
}
if (pendingShareMultiSelect.STARS.length) {
setMultiSelectValues(hotelStarsNode, pendingShareMultiSelect.STARS);
}
if (pendingShareMultiSelect.MEALS.length) {
setMultiSelectValues(mealNode, pendingShareMultiSelect.MEALS);
}
if (hotels.length) {
rebuildHotelSelect(hotels);
}
pendingShareMultiSelect = null;
return hotels;
}
function normalizeShareText(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
function findTourInSearchResults(items, tour) {
tour = tour || {};
if (!Array.isArray(items) || !items.length) {
return null;
}
if (tour.resultId) {
var byResultId = items.find(function (item) {
return String(item.resultId) === String(tour.resultId);
});
if (byResultId) {
return byResultId;
}
}
if (tour.catClaim) {
var byClaim = items.find(function (item) {
return String(item.catClaim) === String(tour.catClaim);
});
if (byClaim) {
return byClaim;
}
}
var candidates = items.filter(function (item) {
if (tour.hotel) {
var hotelNeedle = normalizeShareText(tour.hotel);
var hotelHay = normalizeShareText(item.hotel);
if (hotelNeedle && hotelHay && hotelHay.indexOf(hotelNeedle) < 0 && hotelNeedle.indexOf(hotelHay) < 0) {
return false;
}
}
if (tour.dateFrom && toIsoDate(item.dateFrom) !== toIsoDate(tour.dateFrom)) {
return false;
}
if (tour.nights && String(item.nights) !== String(tour.nights)) {
return false;
}
if (tour.flightInCode && item.flightInCode && String(item.flightInCode) !== String(tour.flightInCode)) {
return false;
}
if (tour.flightOutCode && item.flightOutCode && String(item.flightOutCode) !== String(tour.flightOutCode)) {
return false;
}
if (tour.city && item.city && normalizeShareText(item.city) !== normalizeShareText(tour.city)) {
return false;
}
return true;
});
if (candidates.length === 1) {
return candidates[0];
}
if (candidates.length > 1 && tour.catClaim) {
var byClaimLoose = candidates.find(function (item) {
return String(item.catClaim) === String(tour.catClaim);
});
if (byClaimLoose) {
return byClaimLoose;
}
}
return candidates.length ? candidates[0] : null;
}
function formatTourSearchCriteria(tour, resultsCount, pagesSearched) {
tour = tour || {};
var rows = [
["Отель", tour.hotel ? tour.hotel + (tour.star ? " " + tour.star : "") : "-"],
["Страна", tour.country || "-"],
["Курорт", tour.city || "-"],
["Дата заезда", tour.dateFrom || "-"],
["Ночей", tour.nights || "-"],
["Программа", tour.tour || "-"],
["Питание", tour.meal || "-"],
["Номер", tour.room || "-"],
["Перелёт туда", tour.flightInCode || "-"],
["Перелёт обратно", tour.flightOutCode || "-"],
["Багаж", tour.baggage || "-"]
];
var html =
'В результатах поиска (' +
String(resultsCount || 0) +
" туров на " +
String(pagesSearched || 1) +
" стр.) не удалось найти совпадение по критериям:
" +
'';
rows.forEach(function (row) {
html += "" + escapeHtml(row[0]) + ": " + escapeHtml(row[1]) + " ";
});
html += " ";
return html;
}
function ensureInfoModal() {
var existing = document.getElementById("etwInfoModal");
if (existing) {
return existing;
}
var modal = document.createElement("div");
modal.id = "etwInfoModal";
modal.className = "etw-tour-modal etw-info-modal";
modal.innerHTML =
'
' +
'";
document.body.appendChild(modal);
modal.querySelector(".etw-tour-modal__backdrop").addEventListener("click", function () {
modal.classList.remove("is-open");
});
modal.querySelector(".etw-tour-modal__close").addEventListener("click", function () {
modal.classList.remove("is-open");
});
modal.querySelector(".etw-info-modal__ok").addEventListener("click", function () {
modal.classList.remove("is-open");
});
return modal;
}
function showInfoModal(title, htmlBody, options) {
options = options || {};
var modal = ensureInfoModal();
modal.querySelector(".etw-info-modal__title").textContent = title || "Сообщение";
modal.querySelector(".etw-info-modal__body").innerHTML = htmlBody || "";
modal.classList.toggle("etw-info-modal_no-footer", !!options.hideFooter);
modal.classList.add("is-open");
return modal;
}
function showShareLinkModal(shareData) {
shareData = shareData || {};
var shareUrl = shareData.shareUrl || "";
var expiresAt = shareData.expiresAt || "";
var expiresText = expiresAt ? new Date(expiresAt).toLocaleString("ru-RU") : "через 7 дней";
var html =
'Скопируйте ссылку и отправьте клиенту. По ней откроется этот тур с теми же параметрами поиска.
' +
'Ссылка хранится не более 7 дней и после этого перестанет работать. Даже в срок цена и наличие мест могут измениться — это ориентир, а не гарантия бронирования.
' +
'Ссылка на тур ' +
'' +
' ' +
'Копировать ' +
"
" +
'Действует до: ' +
escapeHtml(expiresText) +
"
" +
'
';
var modal = showInfoModal("Поделиться туром", html, { hideFooter: true });
var copyBtn = modal.querySelector(".etw-share-modal__copy");
var input = modal.querySelector(".etw-share-modal__input");
var status = modal.querySelector(".etw-share-modal__status");
if (copyBtn && input) {
copyBtn.addEventListener("click", function () {
input.focus();
input.select();
var done = function () {
if (status) {
status.textContent = "Ссылка скопирована.";
}
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrl).then(done).catch(function () {
try {
document.execCommand("copy");
done();
} catch (e) {
if (status) {
status.textContent = "Не удалось скопировать автоматически.";
}
}
});
} else {
try {
document.execCommand("copy");
done();
} catch (e2) {
if (status) {
status.textContent = "Скопируйте ссылку вручную.";
}
}
}
});
}
}
async function createAndShowShareLink(context, item) {
try {
var payloadData = buildSharePayloadData(context, item);
var shareResult = await createShareLinkRequest(JSON.stringify(payloadData));
showShareLinkModal(shareResult);
} catch (error) {
showInfoModal("Не удалось создать ссылку", "" + escapeHtml(error.message) + "
");
}
}
function scrollToTourLeadForm(modal) {
modal = modal || document.getElementById("etwTourModal");
if (!modal) {
return;
}
var scrollRoot = modal.querySelector(".etw-tour-modal__body");
var form = modal.querySelector(".etw-agent-form");
if (!scrollRoot || !form) {
return;
}
var top = form.offsetTop;
if (typeof scrollRoot.scrollTo === "function") {
scrollRoot.scrollTo({ top: Math.max(0, top - 8), behavior: "smooth" });
} else {
scrollRoot.scrollTop = Math.max(0, top - 8);
}
var nameInput = form.querySelector('input[name="name"]');
if (nameInput) {
window.setTimeout(function () {
try {
nameInput.focus({ preventScroll: true });
} catch (e) {
nameInput.focus();
}
}, 280);
}
}
function attachShareTourButton(body, context, item) {
var modal = document.getElementById("etwTourModal");
if (!modal) {
return;
}
var oldBodyShare = body && body.querySelector(".etw-share-tour");
if (oldBodyShare && oldBodyShare.parentNode) {
oldBodyShare.parentNode.removeChild(oldBodyShare);
}
var link = modal.querySelector(".etw-tour-modal__share");
if (!link) {
return;
}
link.onclick = function (event) {
event.preventDefault();
createAndShowShareLink(context, item).catch(function () {});
};
}
async function restoreFromSharePayload(payload) {
if (!payload || !payload.search || !payload.search.TOWNFROMINC) {
throw new Error("Некорректные данные ссылки.");
}
applySearchParamsToForm(payload.search);
if (payload.search.PRICEPAGE) {
currentSearchPage = Number(payload.search.PRICEPAGE) || 1;
} else if (payload.tour && payload.tour.pricePage) {
currentSearchPage = Number(payload.tour.pricePage) || 1;
} else {
currentSearchPage = 1;
}
await refreshControls();
var hotelsToRestore = applyPendingShareMultiSelects();
if (hotelsToRestore.length) {
await refreshControls();
setMultiSelectValues(hotelSelectNode, hotelsToRestore);
}
if (progressTextNode) {
progressTextNode.textContent = " - восстанавливаем тур по ссылке...";
}
setLoading(true, "40%");
var lookup = await findTourAcrossSearchPages(payload.tour || {}, payload.search || {});
setLoading(true, "90%");
if (!lookup.matched) {
if (lookup.result && lookup.result.items) {
currentSearchPage = lookup.result.pager.current || 1;
renderResults(lookup.result.items, lookup.result.pager);
}
showInfoModal(
"Тур не найден",
formatTourSearchCriteria(payload.tour || {}, lookup.totalSeen || 0, lookup.pagesSearched || 0)
);
clearShareHash();
setLoading(false);
return;
}
currentSearchPage = lookup.page || 1;
renderResults(lookup.result.items, lookup.result.pager);
clearShareHash();
setLoading(false);
refreshAllFormstoneSelects(false);
await openTourModal(lookup.matched, { shareRestore: payload.actualize || {} });
}
async function tryRestoreFromShareHash() {
var code = getShareHashCode();
if (!code) {
return;
}
if (progressTextNode) {
progressTextNode.textContent = " - загружаем тур по ссылке...";
}
tableContainer.innerHTML = "";
setLoading(true, "15%");
try {
var resolved = await fetchShareResolve(code);
if (!resolved.ok) {
var title = resolved.error === "expired" ? "Ссылка устарела" : "Ссылка недоступна";
var body =
resolved.error === "expired"
? "Срок хранения ссылки истёк (не более 7 дней). Попросите агента отправить новую ссылку на актуальный тур.
"
: "" + escapeHtml(resolved.message || "Короткая ссылка не найдена или повреждена.") + "
";
showInfoModal(title, body);
clearShareHash();
return;
}
var payload = parseSharePayload(resolved.data && resolved.data.payload);
if (!payload || !payload.search || !payload.search.TOWNFROMINC) {
showInfoModal("Ссылка недоступна", "Не удалось прочитать сохранённые параметры тура.
");
clearShareHash();
return;
}
await restoreFromSharePayload(payload);
} catch (error) {
showInfoModal("Ошибка восстановления", "" + escapeHtml(error.message) + "
");
clearShareHash();
} finally {
setLoading(false);
}
}
function bindTourModalHeaderActions(modal) {
if (!modal || modal.getAttribute("data-header-bound") === "1") {
return;
}
modal.setAttribute("data-header-bound", "1");
modal.addEventListener("click", function (event) {
var target = event.target;
if (target && target.classList && target.classList.contains("etw-modal-order")) {
scrollToTourLeadForm(modal);
}
});
}
function ensureTourModalHeaderLayout(modal) {
if (!modal) {
return;
}
var header = modal.querySelector(".etw-tour-modal__header");
var closeBtn = modal.querySelector(".etw-tour-modal__close");
if (!header || !closeBtn) {
return;
}
var right = modal.querySelector(".etw-tour-modal__header-right");
if (!right) {
right = document.createElement("div");
right.className = "etw-tour-modal__header-right";
header.appendChild(right);
}
if (!right.contains(closeBtn)) {
right.appendChild(closeBtn);
}
// Цена/Заказать переехали в тело формы — убираем из шапки.
Array.prototype.slice.call(right.querySelectorAll(".etw-modal-price-block, .etw-modal-price, .etw-modal-order")).forEach(function (node) {
if (node && node.parentNode) {
node.parentNode.removeChild(node);
}
});
var shareLink = right.querySelector(".etw-tour-modal__share");
if (!shareLink) {
shareLink = document.createElement("a");
shareLink.href = "#";
shareLink.className = "etw-tour-modal__share";
shareLink.textContent = "Поделиться туром";
right.insertBefore(shareLink, closeBtn);
} else if (shareLink.nextSibling !== closeBtn) {
right.insertBefore(shareLink, closeBtn);
}
bindTourModalHeaderActions(modal);
}
function ensureTourModal() {
var existing = document.getElementById("etwTourModal");
if (existing) {
ensureTourModalHeaderLayout(existing);
return existing;
}
var modal = document.createElement("div");
modal.id = "etwTourModal";
modal.className = "etw-tour-modal";
modal.innerHTML =
'
' +
'";
document.body.appendChild(modal);
bindTourModalHeaderActions(modal);
modal.querySelector(".etw-tour-modal__backdrop").addEventListener("click", function () {
modal.classList.remove("is-open");
});
modal.querySelector(".etw-tour-modal__close").addEventListener("click", function () {
modal.classList.remove("is-open");
});
return modal;
}
function ensureFlightModal() {
var existing = document.getElementById("etwFlightModal");
if (existing) {
return existing;
}
var modal = document.createElement("div");
modal.id = "etwFlightModal";
modal.className = "etw-tour-modal";
modal.innerHTML =
'
' +
'";
document.body.appendChild(modal);
modal.querySelector(".etw-tour-modal__backdrop").addEventListener("click", function () {
modal.classList.remove("is-open");
});
modal.querySelector(".etw-tour-modal__close").addEventListener("click", function () {
modal.classList.remove("is-open");
});
return modal;
}
function buildTourHotelPlace(item, context) {
context = context || {};
item = item || {};
var hotelName = item.hotel || "-";
if (item.star) {
hotelName += " " + item.star;
}
var placeLine = [context.city || item.city, context.country || item.country].filter(Boolean).join(", ");
return { hotelName: hotelName, placeLine: placeLine };
}
function setTourModalTitle(modal, item, context) {
if (!modal) {
return;
}
var titleNode = modal.querySelector(".etw-tour-modal__title");
if (!titleNode) {
return;
}
var info = buildTourHotelPlace(item, context);
titleNode.innerHTML =
'' + escapeHtml(info.hotelName) + "
" +
(info.placeLine
? '' + escapeHtml(info.placeLine) + "
"
: "");
}
function renderTourSummary(item, context) {
var priceText = formatModalPriceText(context);
var priceClass = "etw-modal-price__value" + (context.priceExact ? " is-exact" : " is-from");
return (
'' +
'
' +
'
' +
'' + escapeHtml(priceText) + " " +
"
" +
'
Заказать ' +
"
" +
renderRequestMeta(context) +
'
' + renderFlightSummaryBlock(context) + "
" +
'
' +
renderOptionsCalcControls(context) +
'
' +
'
' + escapeHtml(context.fareNote || "Уточняем цену и перелет…") + "
" +
'
Проверяем актуальность предложения…
' +
"
" +
"
"
);
}
function syncFareWarningUi(body, context, state) {
var box = body && body.querySelector(".etw-fare-warning");
var note = body && body.querySelector(".etw-fare-warning__note");
var status = body && body.querySelector(".etw-actualize-status");
if (!box) {
return;
}
var mode = state || "loading";
box.className = "etw-fare-warning etw-fare-warning_" + mode;
if (note) {
var noteText = context.fareNote || "";
if (mode === "ok" && (!noteText || /актуализа/i.test(noteText))) {
noteText = "Цена и перелет подтверждены.";
}
if (mode === "loading" && !noteText) {
noteText = "Уточняем цену и перелет…";
}
note.textContent = noteText;
note.style.display = noteText ? "" : "none";
}
if (status && mode === "ok") {
// Без дубля маршрута — он уже в блоке авиабилетов.
var statusText = context.optionsCalcNote ? String(context.optionsCalcNote) : "";
status.textContent = statusText;
if (!statusText.trim()) {
status.style.display = "none";
} else {
status.style.display = "";
}
} else if (status) {
status.style.display = "";
}
}
function refreshModalContext(body, context) {
syncModalPrice(context);
var metaGrid = body.querySelector(".etw-tour-summary__meta-grid");
if (metaGrid) {
var tmp = document.createElement("div");
tmp.innerHTML = renderRequestMeta(context);
if (tmp.firstChild) {
metaGrid.parentNode.replaceChild(tmp.firstChild, metaGrid);
}
}
var fareNoteNode = body.querySelector(".etw-fare-warning__note");
if (fareNoteNode && context.fareNote) {
fareNoteNode.textContent = context.fareNote;
fareNoteNode.style.display = "";
}
syncFlightSummaryUi(body, context);
var hiddenPairs = {
priceRub: context.priceRub || "",
priceTour: context.priceTour || "",
priceExact: context.priceExact ? "1" : "0",
tourCurrency: context.tourCurrency || "",
flightInCode: context.flightInCode || "",
flightOutCode: context.flightOutCode || "",
baggage: context.baggage || "",
route: context.route || "",
fareNote: context.fareNote || "",
withInsurance: context.withInsurance ? "1" : "0",
withVisa: context.withVisa ? "1" : "0",
basePriceTour: context.basePriceTour || "",
insuranceAmount: context.insuranceAmount || "0",
visaAmount: context.visaAmount || "0",
baggageRequired: "all",
baggageRequiredLabel: "Не важно",
offerGuid: context.offerGuid || "",
flightDuration: context.flightDuration || "",
flightStops: context.flightStops || "",
flightDelta: context.flightDelta || "0",
flightSegmentsText: context.flightSegmentsText || "",
baggageLabel: context.baggageLabel || ""
};
Object.keys(hiddenPairs).forEach(function (name) {
var input = body.querySelector('input[name="' + name + '"]');
if (input) {
input.value = hiddenPairs[name];
}
});
}
function renderFlightAlternatives(body, alternatives, context) {
updateFlightChangeAvailability(context, alternatives, { ok: true });
syncFlightSummaryUi(body, context);
var holder = body.querySelector(".etw-flight-alts");
if (holder) {
holder.innerHTML = "";
}
}
function hideFlightAlternatives(body, context, meta) {
if (context) {
updateFlightChangeAvailability(context, [], meta || { failed: true });
syncFlightSummaryUi(body, context);
}
var holder = body && body.querySelector(".etw-flight-alts");
if (holder) {
holder.innerHTML = "";
}
}
function startActualizeProgress(statusNode) {
if (!statusNode) {
return function () {};
}
var box = statusNode.closest(".etw-fare-warning");
if (box) {
box.className = "etw-fare-warning etw-fare-warning_loading";
}
var steps = [
"Проверяем актуальность предложения",
"Проверяем актуальность предложения.",
"Проверяем актуальность предложения..",
"Проверяем актуальность предложения..."
];
var idx = 0;
statusNode.className = "etw-actualize-status etw-actualize-status_loading";
statusNode.style.display = "";
statusNode.textContent = steps[idx];
var timer = setInterval(function () {
idx = (idx + 1) % steps.length;
statusNode.textContent = steps[idx];
}, 450);
return function () {
clearInterval(timer);
};
}
function countOfferStops(segments) {
var list = Array.isArray(segments) ? segments : [];
if (!list.length) {
return 0;
}
var byRoute = {};
var hasRouteIndex = false;
list.forEach(function (seg) {
if (seg && (seg.routeIndex === 0 || seg.routeIndex === 1 || seg.routeIndex === "0" || seg.routeIndex === "1")) {
hasRouteIndex = true;
var key = String(Number(seg.routeIndex));
byRoute[key] = (byRoute[key] || 0) + 1;
}
});
if (hasRouteIndex) {
return Object.keys(byRoute).reduce(function (sum, key) {
return sum + Math.max((byRoute[key] || 0) - 1, 0);
}, 0);
}
// Без routeIndex: туда/обратно примерно пополам — пересадки внутри каждой половины.
if (list.length <= 2) {
return 0;
}
var mid = Math.ceil(list.length / 2);
return Math.max(mid - 1, 0) + Math.max(list.length - mid - 1, 0);
}
function ensureFlightPickerStyles() {
var style = document.getElementById("etwFlightPickerStyles");
if (!style) {
style = document.createElement("style");
style.id = "etwFlightPickerStyles";
document.head.appendChild(style);
}
style.textContent =
"#etwFlightModal .etw-flight-alt-top{display:flex!important;flex-direction:row!important;align-items:flex-start!important;justify-content:space-between!important;gap:16px;width:100%}" +
"#etwFlightModal .etw-flight-alt-main{flex:1 1 auto;min-width:0}" +
"#etwFlightModal .etw-flight-alt-price{flex:0 0 auto!important;margin-left:auto!important;min-width:148px;max-width:46%;text-align:right!important;padding:10px 12px;border-radius:8px;background:#fff0e4!important;border:1px solid #e8c4a0!important}" +
"#etwFlightModal .etw-flight-alt-price__label{font-size:12px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:#a04000!important}" +
"#etwFlightModal .etw-flight-alt-price__value{margin-top:4px;font-size:22px!important;font-weight:800!important;line-height:1.15;color:#c0392b!important;white-space:nowrap}" +
"#etwFlightModal .etw-flight-alt-price__total{margin-top:4px;font-size:12px;font-weight:700;color:#3d4f63;white-space:nowrap}" +
"@media (max-width:640px){#etwFlightModal .etw-flight-alt-top{flex-direction:column!important}#etwFlightModal .etw-flight-alt-price{width:100%;max-width:none;margin-left:0!important;text-align:left!important}}";
}
function openFlightAlternativesModal(alternatives, context, parentBody, rerunActualize) {
ensureFlightPickerStyles();
var modal = ensureFlightModal();
var body = modal.querySelector(".etw-tour-modal__body");
var alts = Array.isArray(alternatives) ? alternatives : [];
var withStopsCount = alts.filter(function (alt) {
return countOfferStops(alt && alt.segments) > 0;
}).length;
var renderRows = function (filterMode) {
var filtered = alts.filter(function (alt) {
var stops = countOfferStops(alt && alt.segments);
if (filterMode === "direct") {
return stops <= 0;
}
if (filterMode === "stops") {
return stops > 0;
}
if (filterMode === "with") {
return hasBaggageOption(alt.baggage) === true;
}
if (filterMode === "without") {
return hasBaggageOption(alt.baggage) === false;
}
return true;
});
if (!filtered.length) {
if (filterMode === "stops") {
return 'В этой выдаче GDS нет рейсов с пересадками — только прямые.
';
}
return 'Нет вариантов по выбранному фильтру.
';
}
return filtered.map(function (alt) {
var currency = alt.tourCurrency || context.tourCurrency || "";
var delta = alt.deltaPrice != null && alt.deltaPrice !== ""
? Number(alt.deltaPrice)
: 0;
if (!Number.isFinite(delta)) {
delta = 0;
}
var deltaText = formatFlightDeltaLabel(delta, currency);
var packageBase = packageBaseTourPrice(context);
var totalWithFlight = packageBase > 0
? formatMoneyWithCurrency(packageBase + delta, currency || context.tourCurrency || "")
: "";
var split = splitSegmentsByDirection(alt.segments, context);
var routeLabel = buildRouteFromLegs(split.outbound, split.inbound) || buildRouteLabel(alt.route, context, alt);
var outDur = formatMinutesRu(sumSegmentsDurationMinutes(split.outbound));
var inDur = formatMinutesRu(sumSegmentsDurationMinutes(split.inbound));
var durationText =
[outDur ? "туда " + outDur : "", inDur ? "обратно " + inDur : ""].filter(Boolean).join(", ") ||
alt.durationText ||
formatMinutesRu(alt.durationMinutes);
var stopCount = countOfferStops(alt.segments);
var stopsText = stopCount > 0 ? (stopCount + " пересад.") : "без пересадок";
var outboundCount = split.outbound.length;
var inboundCount = split.inbound.length;
var encodedSegments = encodeURIComponent(JSON.stringify(Array.isArray(alt.segments) ? alt.segments : []));
var detailsLabel = stopCount > 0 ? ("Показать пересадки (" + stopCount + ")") : "Показать детали рейса";
var flightSignature = function (offer) {
var segs = Array.isArray(offer && offer.segments) ? offer.segments : [];
return segs.map(function (seg) {
return [
seg.from || "",
seg.to || "",
seg.flightNumber || "",
seg.departure || "",
seg.arrival || "",
formatBaggageLabel(seg.baggage || (offer && offer.baggage) || "")
].join("~");
}).join("|");
};
var thisSig = flightSignature(alt);
var sameItineraryCount = thisSig
? filtered.filter(function (other) { return flightSignature(other) === thisSig; }).length
: 1;
var priceWhy = sameItineraryCount > 1
? "Тот же рейс — другой тариф авиакомпании (поэтому доплата другая)"
: "";
return '' +
'
' +
'
' +
'
Вариант ' + escapeHtml(String(alt.index || "")) + ": " + escapeHtml(routeLabel) + "
" +
'
Багаж: ' + escapeHtml(formatBaggageLabel(alt.baggage)) + "
" +
'
В пути: ' + escapeHtml(durationText || "уточняется") + " | " + escapeHtml(stopsText) +
" (туда: " + escapeHtml(String(outboundCount || 0)) + ", обратно: " + escapeHtml(String(inboundCount || 0)) + ")
" +
(priceWhy ? '
' + escapeHtml(priceWhy) + "
" : "") +
"
" +
'
' +
'
Доплата к туру
' +
'
' + escapeHtml(deltaText) + "
" +
(totalWithFlight
? '
Итого тур: ' + escapeHtml(totalWithFlight) + "
"
: "") +
"
" +
"
" +
'
' + renderSegmentsStructured(alt.segments, context) + "
" +
'
' +
'' + escapeHtml(detailsLabel) + " " +
'Выбрать ' +
"
" +
"
";
}).join("");
};
body.innerHTML =
'' +
'
Вариантов: ' + escapeHtml(String(alts.length)) +
", с пересадками: " + escapeHtml(String(withStopsCount)) +
(withStopsCount === 0 ? ". GDS вернул только прямые рейсы." : ".") +
"
" +
'
' +
'Все ' +
'Прямые ' +
'С пересадками ' +
'С багажом ' +
'Без багажа ' +
"
" +
'
' + renderRows("all") + "
" +
"
";
modal.classList.add("is-open");
var bindCardEvents = function () {
body.querySelectorAll(".etw-flight-alt-toggle").forEach(function (toggle) {
toggle.addEventListener("click", function () {
var card = toggle.closest(".etw-flight-alt-btn");
if (!card) {
return;
}
var expanded = card.classList.toggle("is-expanded");
var hasStops = card.classList.contains("has-stops");
if (expanded) {
toggle.textContent = hasStops ? "Свернуть пересадки" : "Свернуть детали";
} else {
toggle.textContent = hasStops ? toggle.getAttribute("data-open-label") || "Показать пересадки" : "Показать детали рейса";
}
});
var card = toggle.closest(".etw-flight-alt-btn");
if (card && card.classList.contains("has-stops")) {
toggle.setAttribute("data-open-label", toggle.textContent);
}
});
body.querySelectorAll(".etw-flight-alt-select").forEach(function (btn) {
btn.addEventListener("click", function () {
context.offerGuid = btn.getAttribute("data-offer-guid") || context.offerGuid;
context.tourCurrency = btn.getAttribute("data-currency") || context.tourCurrency;
context.flightInCode = btn.getAttribute("data-in") || context.flightInCode;
context.flightOutCode = btn.getAttribute("data-out") || context.flightOutCode;
context.baggage = btn.getAttribute("data-baggage") || context.baggage;
context.baggageLabel = formatBaggageLabel(context.baggage);
context.route = btn.getAttribute("data-route") || context.route;
context.flightDuration = btn.getAttribute("data-duration") || context.flightDuration;
context.flightStops = btn.getAttribute("data-stops") || context.flightStops;
applyFlightDeltaToTourPrice(context, btn.getAttribute("data-delta") || "0");
context.fareNote = "Выбран альтернативный перелет. Пересчитываем цену…";
var segments = parseSegmentsSafe(btn.getAttribute("data-segments"));
context.flightSegmentsJson = JSON.stringify(segments);
context.flightSegmentsText = formatSegmentsText(segments);
refreshModalContext(parentBody, context);
syncModalPrice(context);
modal.classList.remove("is-open");
if (typeof rerunActualize === "function") {
rerunActualize().catch(function () {});
return;
}
var statusNode = parentBody.querySelector(".etw-actualize-status");
if (statusNode) {
statusNode.className = "etw-actualize-status etw-actualize-status_ok";
statusNode.innerHTML = "Выбран альтернативный перелет: " + escapeHtml(context.route) + ". " +
"Багаж: " + escapeHtml(formatBaggageLabel(context.baggage)) + ". " +
"В пути: " + escapeHtml(context.flightDuration || "уточняется") + ", " + escapeHtml(context.flightStops || "пересадки: уточняется") + "." +
renderSegmentsStructured(segments, context);
}
});
});
};
body.querySelectorAll(".etw-flight-baggage-filter__btn").forEach(function (filterBtn) {
filterBtn.addEventListener("click", function () {
body.querySelectorAll(".etw-flight-baggage-filter__btn").forEach(function (btn) {
btn.classList.toggle("is-active", btn === filterBtn);
});
var mode = filterBtn.getAttribute("data-bag-filter") || "all";
var listNode = body.querySelector(".etw-flight-alts__list_modal");
if (listNode) {
listNode.innerHTML = renderRows(mode);
bindCardEvents();
}
});
});
bindCardEvents();
}
function renderHotelInfoBlock(content) {
var images = Array.isArray(content.images) ? content.images : [];
var tabs = content.tabs || {};
var desc = tabs["tabs-desc"] || tabs["tabs-place"] || "";
var service = tabs["tabs-service"] || "";
var gallery = "";
if (images.length) {
var first = images[0];
var thumbs = images
.slice(0, 8)
.map(function (src) {
return ' ';
})
.join("");
gallery =
'' +
'
' +
'
' +
thumbs +
"
" +
"
";
}
return (
'' +
gallery +
(desc ? '
' + desc + "
" : "") +
(service ? '
' + service + "
" : "") +
"
"
);
}
function renderAgentForm(context) {
return (
'"
);
}
async function submitLeadForm(formNode) {
var submitBtn = formNode.querySelector(".etw-agent-submit");
var statusNode = formNode.querySelector(".etw-agent-form__status");
var formData = new FormData(formNode);
var params = new URLSearchParams();
formData.forEach(function (value, key) {
params.append(key, value);
});
if (statusNode) {
statusNode.textContent = "Отправляем заявку...";
statusNode.className = "etw-agent-form__status is-loading";
}
if (submitBtn) {
submitBtn.disabled = true;
}
try {
var response = await fetch(buildApiUrl("lead", {}), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body: params.toString()
});
var payload = await response.json();
if (!response.ok || !payload || payload.ok === false) {
throw new Error((payload && payload.message) || "Не удалось отправить заявку.");
}
if (statusNode) {
statusNode.textContent = "Заявка отправлена. Агент скоро свяжется с вами.";
statusNode.className = "etw-agent-form__status is-success";
}
} catch (error) {
if (statusNode) {
statusNode.textContent = "Ошибка отправки: " + error.message;
statusNode.className = "etw-agent-form__status is-error";
}
} finally {
if (submitBtn) {
submitBtn.disabled = false;
}
}
}
async function openTourModal(item, modalOptions) {
modalOptions = modalOptions || {};
var shareRestore = modalOptions.shareRestore || null;
var pendingShareOfferGuid = "";
var modal = ensureTourModal();
var body = modal.querySelector(".etw-tour-modal__body");
var context = buildRequestContext(item);
if (shareRestore) {
if (shareRestore.withInsurance) {
context.withInsurance = true;
}
if (shareRestore.withVisa) {
context.withVisa = true;
}
if (shareRestore.offerGuid) {
pendingShareOfferGuid = String(shareRestore.offerGuid);
}
}
setTourModalTitle(modal, item, context);
modal.classList.add("is-open");
syncModalPrice(context);
body.innerHTML =
renderTourSummary(item, context) +
'Описание отеля
Загрузка...
' +
renderAgentForm(context);
if (jq && jq.fn && jq.fn.checkbox) {
jq(body).find(".sf-field__radio").checkbox();
}
var leadFormNode = body.querySelector(".etw-agent-form");
if (leadFormNode) {
leadFormNode.addEventListener("submit", function (event) {
event.preventDefault();
submitLeadForm(leadFormNode);
});
}
attachShareTourButton(body, context, item);
var runActualize = async function () {
var statusNodeOk = body.querySelector(".etw-actualize-status");
var stopProgress = startActualizeProgress(statusNodeOk);
hideFlightAlternatives(body, context, { loading: true });
try {
var actualizeOptions = {};
if (pendingShareOfferGuid) {
actualizeOptions.skipOfferGuid = true;
actualizeOptions.offerGuidHint = pendingShareOfferGuid;
pendingShareOfferGuid = "";
}
var actualized = await fetchJSON(buildApiUrl("actualize", buildActualizeApiParams(context, item, actualizeOptions)));
stopProgress();
statusNodeOk = body.querySelector(".etw-actualize-status");
var data = actualized && actualized.data ? actualized.data : null;
if (data && data.ok && data.matched) {
var matched = data.matched;
// Цена из выдачи SAMO (пакет), не тариф GDS.
applyTourDisplayPrice(
context,
matched.price,
matched.convertedPriceNumber || matched.convertedPrice,
false
);
if (matched.currencyAlias || matched.currency) {
context.tourCurrency = matched.currencyAlias || matched.currency || context.tourCurrency;
}
context.flightInCode = matched.freights && matched.freights.econom ? String(matched.freights.econom.in || context.flightInCode) : context.flightInCode;
context.flightOutCode = matched.freights && matched.freights.econom ? String(matched.freights.econom.out || context.flightOutCode) : context.flightOutCode;
context.baggage = matched.freights && matched.freights.econom ? String(matched.freights.econom.baggage || context.baggage) : context.baggage;
context.route = buildRouteLabel("", context, { flightInCode: context.flightInCode, flightOutCode: context.flightOutCode });
context.fareNote = data.message || "Цена от. Выполнена актуализация.";
if (data.fullActualize && data.fullActualize.ok && data.fullActualize.selected) {
var selected = data.fullActualize.selected;
applyFlightCalcToContext(context, data.fullActualize.flightCalc || null, selected);
var gdsTourPrice = selected.priceTour !== undefined && selected.priceTour !== null && selected.priceTour !== ""
? selected.priceTour
: selected.price;
var gdsRubPrice = selected.priceRub;
var selectedDelta = selected.deltaPrice != null
? selected.deltaPrice
: (data.fullActualize.flightCalc && data.fullActualize.flightCalc.deltaPrice != null
? data.fullActualize.flightCalc.deltaPrice
: context.flightDelta);
// Не затираем цену тура «аэротарифом» 0 / 15 / 299 — берём пакет + доплата за рейс.
if (isPositiveMoney(gdsTourPrice) && !looksLikeFlightOnlyPrice(gdsTourPrice, context.searchPriceTour || context.priceTour)) {
applyTourDisplayPrice(context, gdsTourPrice, gdsRubPrice, true);
if (selectedDelta != null && selectedDelta !== "") {
context.flightDelta = String(parseMoney(selectedDelta) || 0);
}
} else {
applyFlightDeltaToTourPrice(context, selectedDelta);
}
} else if (data.fullActualize && data.fullActualize.flightCalc) {
applyFlightCalcToContext(context, data.fullActualize.flightCalc, null);
applyFlightDeltaToTourPrice(context, data.fullActualize.flightCalc.deltaPrice);
context.priceExact = false;
ensureFallbackTourPrices(context);
} else {
context.priceExact = false;
ensureFallbackTourPrices(context);
}
if (data.fullActualize && data.fullActualize.optionsCalc) {
applyOptionsCalcToContext(context, data.fullActualize.optionsCalc, data.fullActualize.selected || null);
if (context.optionsCalcWarnings) {
context.fareNote = context.optionsCalcWarnings;
} else if (context.optionsCalcNote) {
context.fareNote = "Цена пересчитана: " + context.optionsCalcNote + ".";
}
} else {
context.basePriceTour = "";
context.insuranceAmount = "0";
context.visaAmount = "0";
context.optionsCalcNote = "";
context.optionsCalcWarnings = "";
}
syncOptionsCalcUi(body, context);
refreshModalContext(body, context);
attachShareTourButton(body, context, item);
if (data.fullActualize && data.fullActualize.ok) {
var alts = Array.isArray(data.fullActualize.alternatives) ? data.fullActualize.alternatives : [];
renderFlightAlternatives(body, alts, context);
} else {
hideFlightAlternatives(body, context, {
failed: true,
message: data.fullActualize && data.fullActualize.message ? data.fullActualize.message : ""
});
}
if (statusNodeOk) {
if (data.fullActualize && data.fullActualize.ok) {
statusNodeOk.className = "etw-actualize-status etw-actualize-status_ok";
context.fareNote = "Цена и перелет подтверждены.";
if (data.fullActualize.alternatives && data.fullActualize.alternatives.length > 1) {
context.fareNote = "Цена подтверждена. Можно сменить перелет.";
}
syncFareWarningUi(body, context, "ok");
} else {
statusNodeOk.className = "etw-actualize-status etw-actualize-status_warn";
var failMsg = (data.fullActualize && data.fullActualize.message)
? String(data.fullActualize.message)
: "";
var warnText = failMsg
? ("Показана цена из поиска. Перелет: " + failMsg)
: "Показана цена из поиска. Точный перелет сейчас уточнить не удалось — агент подтвердит при заявке.";
statusNodeOk.textContent = warnText;
context.fareNote = "Цена ориентировочная";
syncFareWarningUi(body, context, "warn");
var fareNoteWarn = body.querySelector(".etw-fare-warning__note");
if (fareNoteWarn) {
fareNoteWarn.textContent = context.fareNote;
}
}
}
} else if (statusNodeOk) {
hideFlightAlternatives(body, context, {
failed: true,
message: "не удалось обновить цену и варианты перелета"
});
attachShareTourButton(body, context, item);
context.priceExact = false;
ensureFallbackTourPrices(context);
syncModalPrice(context);
context.fareNote = "Не удалось обновить цену";
statusNodeOk.className = "etw-actualize-status etw-actualize-status_warn";
statusNodeOk.textContent = "Цена может измениться. Отправьте заявку — агент проверит наличие мест.";
syncFareWarningUi(body, context, "warn");
}
} catch (e) {
stopProgress();
var statusNodeErr = body.querySelector(".etw-actualize-status");
hideFlightAlternatives(body, context, {
failed: true,
message: "ошибка при уточнении рейса"
});
context.priceExact = false;
ensureFallbackTourPrices(context);
syncModalPrice(context);
context.fareNote = "Не удалось обновить цену";
if (statusNodeErr) {
statusNodeErr.className = "etw-actualize-status etw-actualize-status_warn";
statusNodeErr.textContent = "Цена может измениться. Отправьте заявку — агент проверит наличие мест.";
}
syncFareWarningUi(body, context, "warn");
}
};
context._rerunActualize = runActualize;
bindOptionsCalcControls(body, context, runActualize);
await runActualize();
if (!item.hotelUrl) {
var loadingNodeNoUrl = body.querySelector(".etw-tour-loading");
if (loadingNodeNoUrl) {
loadingNodeNoUrl.textContent = "Для этого тура карточка отеля недоступна.";
}
return;
}
try {
var payload = await fetchJSON(buildApiUrl("hotelcontent", { url: item.hotelUrl }));
var content = payload.data && payload.data.content ? payload.data.content : {};
var loadingNode = body.querySelector(".etw-tour-loading");
if (loadingNode) {
loadingNode.outerHTML = renderHotelInfoBlock(content);
}
var mainImg = body.querySelector(".etw-tour-main img");
body.querySelectorAll(".etw-tour-thumb").forEach(function (btn) {
btn.addEventListener("click", function () {
var src = btn.getAttribute("data-src");
if (mainImg && src) {
mainImg.src = src;
}
});
});
} catch (error) {
var loadingNodeErr = body.querySelector(".etw-tour-loading");
if (loadingNodeErr) {
loadingNodeErr.textContent = "Не удалось загрузить описание отеля.";
}
}
}
function setLoading(isLoading, progress) {
loadingBox.classList.toggle("sf-searchresult__loading_hidden", !isLoading);
progressNode.textContent = progress || "0%";
}
function showDateHints() {
if (dateHintsNode) {
dateHintsNode.classList.remove("etw-date-hints_hidden");
if (calendarAvailability) {
if (lastCheckinList) {
calendarAvailability.updateDates(lastCheckinList);
calendarAvailability.setRange(dateFromNode.value, dateToNode.value);
} else {
dateHintsNode.innerHTML = 'Загружаем календарь...
';
refreshControls().catch(function () {});
}
}
}
}
function hideDateHints() {
if (dateHintsNode) {
dateHintsNode.classList.add("etw-date-hints_hidden");
}
}
function toIsoDate(value) {
if (!value) {
return "";
}
var raw = String(value).trim();
var m = raw.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
if (m) {
return m[3] + "-" + m[2] + "-" + m[1];
}
var date = new Date(raw);
if (isNaN(date.getTime())) {
return raw;
}
return date.toISOString().slice(0, 10);
}
async function loadRates() {
try {
var ratesPayloads = await Promise.all([
fetchJSON(buildApiUrl("rates", { BASE_ALIAS: "USD" })),
fetchJSON(buildApiUrl("rates", { BASE_ALIAS: "EUR" }))
]);
var usdData = ratesPayloads[0] && ratesPayloads[0].data ? ratesPayloads[0].data : {};
var eurData = ratesPayloads[1] && ratesPayloads[1].data ? ratesPayloads[1].data : {};
var usdToRub = usdData.rates && usdData.rates.RUB ? Number(usdData.rates.RUB) : null;
var eurToRub = eurData.rates && eurData.rates.RUB ? Number(eurData.rates.RUB) : null;
if (rateUsdNode) {
rateUsdNode.textContent = Number.isFinite(usdToRub) ? usdToRub.toFixed(2) : "-";
}
if (rateEurNode) {
rateEurNode.textContent = Number.isFinite(eurToRub) ? eurToRub.toFixed(2) : "-";
}
if (ratesDateNode) {
ratesDateNode.textContent = usdData.date || eurData.date || "-";
}
} catch (error) {
if (rateUsdNode) {
rateUsdNode.textContent = "-";
}
if (rateEurNode) {
rateEurNode.textContent = "-";
}
if (ratesDateNode) {
ratesDateNode.textContent = "-";
}
}
}
async function loadStat() {
await sendStatEvent("start");
}
async function sendStatEvent(eventType) {
try {
await fetchJSON(
buildApiUrl("stat", {
host: statHost,
agentEmail: agentEmail || "",
widgetName: widgetName,
widgetVersion: widgetVersion,
event: eventType || "start"
})
);
} catch (error) {
// Stat collection should not break search widget startup.
}
}
function setDatesFromMeta(data) {
var today = new Date();
var nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
if (!dateFromNode.value) {
dateFromNode.value = nextWeek.toISOString().slice(0, 10);
}
if (!dateToNode.value) {
dateToNode.value = new Date(nextWeek.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
}
if (data && data.base && data.base.CHECKIN_BEG) {
dateFromNode.value = data.base.CHECKIN_BEG;
}
if (data && data.base && data.base.CHECKIN_END) {
dateToNode.value = data.base.CHECKIN_END;
}
}
async function loadMeta() {
var payload = await fetchJSON(buildApiUrl("meta", {}));
var data = payload.data || {};
fullTownFroms = data.townFroms || data.TOWNFROMS || [];
buildCountryFromOptions(fullTownFroms);
filterTownFromsByCountryFrom();
syncDepartureFieldPresentation();
fillSelect(countryNode, data.states || data.STATES || [], "Любая");
// Страну тура фиксируем до первого controls — иначе тип тура/туры приходят пустыми.
if (defaultTourCountry && selectHasOption(countryNode, defaultTourCountry)) {
countryNode.value = String(defaultTourCountry);
} else if (data.base && data.base.STATEINC && selectHasOption(countryNode, data.base.STATEINC)) {
countryNode.value = String(data.base.STATEINC);
} else {
var metaSelectedState = (data.states || data.STATES || []).find(function (item) {
return Number(item.selected) === 1;
});
if (metaSelectedState && blacklistTourCountries.indexOf(String(metaSelectedState.id || "")) < 0) {
countryNode.value = String(metaSelectedState.id);
}
}
updateFormstoneSelect(countryNode, true);
tourTypeCatalog = data.tourTypes || data.TOUR_TYPES || [];
fillSelect(tourTypeNode, tourTypeCatalog, "Любой");
fillSelect(tourNode, data.tours || data.TOURS || [], "Любой");
fillMultiSelect(mealNode, data.meals || data.MEALS || [], []);
fillNightsFromObject(data.nights || []);
setDatesFromMeta(data);
if (data.base) {
if (data.base.TOWNFROMINC) {
var baseTownId = String(data.base.TOWNFROMINC);
var selectedTown = fullTownFroms.find(function (town) {
return String(town.id) === baseTownId;
});
// Не перетираем уже выбранную страну вылета (например Россия)
// каталожным base из другой страны (часто Ереван) — иначе value
// города становится невалидным и Formstone врёт подписью.
if (selectedTown && selectedTown.stateFromKey && !countryFromNode.value) {
countryFromNode.value = String(selectedTown.stateFromKey);
filterTownFromsByCountryFrom();
}
if (!defaultDepartureCity && selectHasOption(depCityNode, baseTownId)) {
depCityNode.value = baseTownId;
updateFormstoneSelect(depCityNode, true);
}
syncDepartureFieldPresentation();
}
}
ensureDepartureCitySelected();
await refreshControls();
refreshAllFormstoneSelects(false);
ensureDepartureCitySelected();
syncDepartureFieldPresentation();
}
async function executeSearch(options) {
options = options || {};
if (!options.skipStat) {
sendStatEvent("search");
}
var result = await fetchSearchResults(options);
if (!options.skipRender) {
renderResults(result.items, result.pager);
}
return result;
}
async function startSearch(event) {
if (event) {
event.preventDefault();
}
currentSearchPage = 1;
searchButton.disabled = true;
progressTextNode.style.display = "";
setLoading(true, "25%");
tableContainer.innerHTML = "";
try {
setLoading(true, "70%");
await executeSearch();
setLoading(true, "100%");
} catch (error) {
tableContainer.innerHTML = 'Ошибка загрузки данных: ' + error.message + "
";
} finally {
setTimeout(function () {
setLoading(false, "100%");
}, 500);
searchButton.disabled = false;
}
}
form.addEventListener("submit", startSearch);
tableContainer.addEventListener("click", function (event) {
var pageBtn = event.target.closest(".etw-search-pager__btn");
if (pageBtn) {
event.preventDefault();
var page = Number(pageBtn.getAttribute("data-page") || 1);
loadSearchPage(page).catch(function () {});
return;
}
var priceBtn = event.target.closest(".etw-price-btn");
if (priceBtn) {
event.preventDefault();
var resultKey = priceBtn.getAttribute("data-result-key") || "";
var item = renderResultMap[resultKey];
if (item) {
openTourModal(item);
}
return;
}
var link = event.target.closest(".etw-hotel-link");
if (!link) {
return;
}
event.preventDefault();
var encodedUrl = link.getAttribute("data-hotel-url") || "";
var encodedTitle = link.getAttribute("data-hotel-title") || "";
var hotelUrl = "";
var hotelTitle = "";
try {
hotelUrl = decodeURIComponent(encodedUrl);
} catch (err) {
hotelUrl = encodedUrl;
}
try {
hotelTitle = decodeURIComponent(encodedTitle);
} catch (err2) {
hotelTitle = encodedTitle;
}
if (hotelUrl) {
openHotelModal(hotelUrl, hotelTitle);
}
});
bindCascadeSelectHandlers();
Promise.all([loadMeta(), loadRates(), loadStat()])
.then(function () {
initFormstoneInputs();
setAdvancedFiltersVisible(defaultAdvancedOpen);
updateChildAgeState();
bindCascadeSelectHandlers();
refreshAllFormstoneSelects(false);
return tryRestoreFromShareHash();
})
.then(function () {
refreshAllFormstoneSelects(false);
syncDepartureFieldPresentation();
})
.catch(function (error) {
tableContainer.innerHTML = 'Не удалось загрузить справочники: ' + error.message + "
";
});
hotelFilterNode.addEventListener("input", function () {
hideDateHints();
filterHotelOptions();
});
if (advancedFiltersNode) {
advancedFiltersNode.addEventListener("change", function () {
setAdvancedFiltersVisible(advancedFiltersNode.checked);
if (advancedFiltersNode.checked) {
refreshControls().catch(function () {});
}
});
if (jq && jq.fn) {
jq(advancedFiltersNode).on("change.etours", function () {
setAdvancedFiltersVisible(advancedFiltersNode.checked);
if (advancedFiltersNode.checked) {
refreshControls().catch(function () {});
}
});
}
}
childsNode.addEventListener("change", function () {
hideDateHints();
updateChildAgeState();
});
if (jq && jq.fn) {
jq(childsNode).on("change.etours", function () {
updateChildAgeState();
});
}
Array.prototype.forEach.call(priceModeNodes, function (node) {
node.addEventListener("change", function () {
hideDateHints();
});
});
dateFromNode.addEventListener("change", function () {
showDateHints();
if (calendarAvailability) {
calendarAvailability.setRange(dateFromNode.value, dateToNode.value);
}
refreshControls().catch(function () {});
});
dateFromNode.addEventListener("focus", showDateHints);
dateFromNode.addEventListener("click", showDateHints);
dateToNode.addEventListener("change", function () {
showDateHints();
if (calendarAvailability) {
calendarAvailability.setRange(dateFromNode.value, dateToNode.value);
}
refreshControls().catch(function () {});
});
dateToNode.addEventListener("focus", showDateHints);
dateToNode.addEventListener("click", showDateHints);
document.addEventListener("click", function (event) {
if (!wrapper || !dateHintsNode) {
return;
}
var t = event.target;
var path = typeof event.composedPath === "function" ? event.composedPath() : [];
if (path.length && (path.indexOf(dateHintsNode) >= 0 || path.indexOf(dateFromNode) >= 0 || path.indexOf(dateToNode) >= 0)) {
return;
}
if (t === dateFromNode || t === dateToNode) {
return;
}
if (dateHintsNode.contains(t)) {
return;
}
hideDateHints();
});
wrapper.addEventListener("focusin", function (event) {
var t = event.target;
if (!t) {
return;
}
if (t === dateFromNode || t === dateToNode) {
return;
}
if (dateHintsNode && dateHintsNode.contains(t)) {
return;
}
hideDateHints();
});
})();