1var destination_is_departure = false; 2var destination_is_departure_mobile = false; 3var suggestionTimer; 4var isTyping = false; 5 6// DEV-462: varsta maxima a unui copil = 17 ani (rationamentul Eurosite: adult 7// de la 18 ani in sus). Varsta e cea de la data INTOARCERII, nu de la plecare â 8// la fel se valideaza si data nasterii introdusa in checkout. 9var KIDS_MAX_AGE = 17; 10 11// Un singur loc care construieste select-ul de varsta (desktop + mobil), ca sa 12// nu mai avem doua liste de <option> hardcodate care se pot desincroniza. 13function kidsAgeSelectHtml() { 14 var options = '<option value="">Varsta copil</option>'; 15 for (var age = 0; age <= KIDS_MAX_AGE; age++) { 16 options += '<option value="' + age + '">' + age + '</option>'; 17 } 18 19 return '<div class="col-md-6 mb-3">' + 20 '<select name="kids_age[]" class="form-control">' + options + '</select>' + 21 '</div>'; 22} 23 24// ========================================================================== 25// Validare search-bar: blocheazÄ submit dacÄ destinaÈia (sau plecarea pentru 26// bilete/charter) nu e completatÄ. AfiÈeazÄ feedback: bordurÄ roÈie pe câmp 27// + toast în partea de sus â în loc de 403 dupÄ submit gol. 28// FuncÈioneazÄ cross-searchbox (home page + searchbox-*.blade variants). 29// ========================================================================== 30(function injectSearchboxValidationStyles() { 31 if (document.getElementById('searchbox-validation-styles')) return; 32 var css = '' 33 + '.searchbox-invalid { border: 2px solid #E01717 !important; animation: searchboxShake 0.35s ease-in-out; }' 34 + '@keyframes searchboxShake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }' 35 + '.searchbox-error-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #E01717; color: #fff; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.15); z-index: 9999; font-weight: 600; font-size: 14px; animation: searchboxToastIn 0.25s ease-out; }' 36 + '@keyframes searchboxToastIn { from { opacity: 0; transform: translate(-50%, -10px); } to { opacity: 1; transform: translate(-50%, 0); } }' 37 // DEV-577: fara tremurat / alunecare pentru cine a cerut miscare redusa 38 + '@media (prefers-reduced-motion: reduce) { .searchbox-invalid, .searchbox-error-toast { animation: none; } }'; 39 var style = document.createElement('style'); 40 style.id = 'searchbox-validation-styles'; 41 style.textContent = css; 42 document.head.appendChild(style); 43})(); 44 45// DEV-577 â toast-ul comun al blocajelor de mai jos. role="alert": mesajul e anuntat si de 46// cititoarele de ecran (pana acum singurul semnal era vizual, 3 secunde). 47function searchboxErrorToast(msg) { 48 $('.searchbox-error-toast').remove(); 49 var $toast = $('<div class="searchbox-error-toast" role="alert"></div>').text(msg).appendTo('body'); 50 setTimeout(function () { $toast.fadeOut(200, function () { $(this).remove(); }); }, 3000); 51} 52 53// ========================================================================== 54// DEV-577 â Sejururi / 7+7 (POST /search/packs): destinatia, orasul de plecare SI data sunt 55// obligatorii. Serverul raspunde 403 fara codul destinatiei, fara lista de plecari sau fara o 56// data care e plecare a perechii destinatie + oras; inainte, fara data, Eurosite primea CheckIn 57// gol si listingul iesea gol (âNu am gasit hoteluri disponibile in perioada selectata!"). 58// Regula de stare: textul vizibil nu e o alegere. Codurile (.destination-code / .departure-code), 59// lista de plecari (.departure-dates + data-departure-dates) si data (.start-at / .end-at) se scriu 60// doar la click pe o sugestie / pe o zi din calendar si se golesc cand perechea se schimba â 61// altfel âOman" tastat peste Cancun pleca cu codul CNC, iar data altei destinatii ajungea la server. 62// Totul e legat de action-ul formularului, nu de clasele sugestiilor: Bilete de avion (banner, 63// tabul mobil) si searchbox-urile de buget folosesc aceleasi clase, dar posteaza in alta parte. 64// ========================================================================== 65function isPackSearchForm($form) { 66 return $form.length > 0 && ($form.attr('action') || '').toLowerCase().indexOf('/search/packs') !== -1; 67} 68 69// âAzi" ca Y-m-d in fusul aplicatiei (Europe/Bucharest), nu dupa ceasul browserului: serverul 70// valideaza `after_or_equal:today` in fusul lui, iar un vizitator din alt fus n-are voie sa aleaga 71// ziua de ieri a Bucurestiului. Folosit si de filtrul calendarului (suggestion-calendar). 72var packTodayFormatter = null; 73function packTodayYmd() { 74 try { 75 packTodayFormatter = packTodayFormatter || new Intl.DateTimeFormat('en-CA', { 76 timeZone: 'Europe/Bucharest', year: 'numeric', month: '2-digit', day: '2-digit' 77 }); 78 var p = {};
79 packTodayFormatter.formatToParts(new Date()).forEach(function (x) { p[x.type] = x.value; }); 80 if (p.year && p.month && p.day) return p.year + '-' + p.month + '-' + p.day; 81 } catch (err) { /* Intl fara fusuri orare: cadem pe ceasul local */ } 82 var d = new Date(); 83 return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); 84} 85 86// Cautarea duce in listing (url = /rezultate-cautare-*, sau lipsa), nu pe pagina unui hotel? Aceeasi 87// regula ca Search::targetsPackListing(): doar listingul cere ca data sa fie o plecare a perechii 88// trimise; pe PDP serverul o scuteste (ES e intrebat pe produs, nu pe zona). 89function packTargetsListing($form) { 90 var raw = $.trim($form.find('input[name="url"]').last().val() || ''); 91 var abs = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^\/?#]*([^?#]*)/i.exec(raw); // ca parse_url(â¦, PHP_URL_PATH) 92 var path = abs ? abs[1] : raw.split(/[?#]/)[0]; 93 return path === '' || path.indexOf('/rezultate-cautare-') === 0; 94} 95 96// Containerul care tine starea perechii: .search-bar pe desktop si PDP, .search-type-content 97// (#pack-content) in offcanvas-ul mobil â acolo scrie si updateCalendarDates data-departure-dates. 98function packSearchScope(el) { 99 var $scope = $(el).closest('.search-bar'); 100 if (!$scope.length) $scope = $(el).closest('.search-type-content'); // nu `closest('a, b')`: formularul e mai aproape 101 return $scope.length ? $scope : $(el).closest('form'); 102} 103 104// Zilele din lista de plecari ('{"2026-10-04":["7","14"]}'). Handlerele de click scriu si '"[]"' 105// (JSON.stringify('[]')) â un sir JSON care contine alt JSON, deci il despachetam. 106// [] = lista goala, null = format necunoscut (nu judecam pe el; decide serverul). Doar zile exact 107// Y-m-d: serverul compara `start_at` cu `Value` la egalitate (PackDestination::departsOn). 108function packDepartureDays(raw) { 109 var v = raw; 110 for (var i = 0; i < 2 && typeof v === 'string'; i++) { 111 if (!$.trim(v)) return []; 112 try { v = JSON.parse(v); } catch (err) { return null; } 113 } 114 if (!v || typeof v !== 'object') return []; 115 var days = Array.isArray(v) ? v.map(function (d) { return d && d.Value ? d.Value : d; }) : Object.keys(v); 116 return days.map(String).filter(function (d) { return /^\d{4}-\d{2}-\d{2}$/.test(d); }); 117} 118 119// Goleste data aleasa: textul vizibil, valorile ISO si selectia din calendar (clear() re-randeaza 120// si zilele blocate dupa lista de plecari curenta). 121function resetPackDate($scope) { 122 $scope.find('.start-at, .end-at').val(''); 123 $scope.find('input.dates, input.dates-mobile').val(''); 124 $scope.find('.calendar-preview').text('Alege datele'); 125 var pickerId = $scope.attr('data-picker') || $scope.find('[data-picker]').first().attr('data-picker'); 126 var picker = pickerId ? window[pickerId] : null; 127 if (picker && typeof picker.clear === 'function') picker.clear(); 128} 129 130// Perechea destinatie + oras s-a schimbat: lista de plecari si data nu mai sunt ale ei. Revenim la 131// starea paginii proaspete (data-departure-dates gol). Doar daca era ceva de golit â handlerul 132// ruleaza la fiecare tasta si nu re-randam calendarul degeaba. 133function resetPackPair($scope) { 134 var hadState = $.trim($scope.attr('data-departure-dates') || '') !== '' 135 || $scope.find('.departure-dates, .start-at, .end-at, input.dates, input.dates-mobile') 136 .filter(function () { return $.trim(this.value || '') !== ''; }).length > 0; 137 if (!hadState) return; 138 $scope.attr('data-departure-dates', ''); 139 $scope.find('.departure-dates').val(''); 140 resetPackDate($scope); 141} 142 143// Prima problema a formularului, in ordinea campurilor (destinatie -> oras de plecare -> data). 144// Citim ultimul camp cu numele respectiv: la campuri duplicate (mobil, listing) PHP il ia pe ultimul. 145function packSearchIssue($form) { 146 var val = function (name) { return $.trim($form.find('input[name="' + name + '"]').last().val() || ''); }; 147 var destinationIssue = { field: 'destination', message: 'Te rugÄm sÄ alegi destinaÈia din listÄ.' }; 148 var departureIssue = { field: 'departure', message: 'Te rugÄm sÄ alegi oraÈul de plecare din listÄ.' }; 149 150 if (!val('destination_code')) return destinationIssue; 151 if (!val('departure_code')) return departureIssue; 152 153 // Lista de plecari o scrie a doua alegere din pereche: orasul (ordinea normala) sau destinatia 154 // (dupa butonul de inversare) â acolo trimitem utilizatorul. Spre listing lista trebuie sa aiba 155 // plecari; spre pagina unui hotel serverul cere doar campul completat (`required`). 156 var toListing = packTargetsListing($form); 157 var rawDays = $form.find('input[name="departure_dates"]').last().val(); 158 var days = packDepartureDays(rawDays); 159 if (toListing ? (days !== null && days.length === 0) : !$.trim(rawDays || '')) { 160 var reversed = $form.closest('#offcanvasMobile').length ? destination_is_departure_mobile : destination_is_departure; 161 return reversed ? destinationIssue : departureIssue; 162 } 163 164 // Data, cu regulile serverului: Y-m-d exact, nu in trecut (azi in Europe/Bucharest â plecarea de 165 // ieri sta in departure_dates pana la sync-ul pack-dates de la 02:00) si, spre listing, o plecare a 166 // perechii (listingul precompleteaza data cautarii de pe care vii; plecarea poate fi intre timp 167 // anulata). Orice alta valoare = data expirata: o golim si deschidem calendarul. 168 var startAt = val('start_at'); 169 if (!startAt) { 170 return { field: 'dates', message: 'Te rugÄm sÄ alegi data plecÄrii înainte de cÄutare.', stale: false }; 171 } 172 if (!/^\d{4}-\d{2}-\d{2}$/.test(startAt) || startAt < packTodayYmd() 173 || (toListing && days && days.indexOf(startAt) === -1)) { 174 return { field: 'dates', message: 'Te rugÄm sÄ alegi data plecÄrii înainte de cÄutare.', stale: true }; 175 } 176 return null; 177} 178 179// Marcheaza campul (pe mobil si randul din meniul principal care ii deschide submeniul), il deschide 180// si sterge marcajul la prima corectare. Nu pe `focus`: deschiderea dropdown-ului muta focusul in 181// input si marcajul ar disparea inainte sa se vada. 182function flagPackSearchIssue($form, issue) { 183 var ns = '.dev577invalid'; 184 var $input = $form.find('input[name="' + issue.field + '"]').filter(function () { 185 return $(this).attr('type') !== 'hidden'; 186 }).first(); 187 var $sub = $input.closest('.submenu-container'); 188 var $menuItem = $sub.attr('id') ? $form.find('.menu-item[data-target="' + $sub.attr('id') + '"]').first() : $(); 189 var $box = $sub.length ? $sub : $input.closest('.destination-section, .departure-section'); 190 var $flagged = $input.add($menuItem); 191 var unflag = function () { 192 $flagged.removeClass('searchbox-invalid'); 193 $input.off(ns); 194 $box.off(ns); 195 }; 196 197 // Marcajele ramase de la un blocaj anterior pe destinatie / plecare / data nu mai sunt valabile 198 // (textele sunt completate, altfel nu ajungeam aici): ramane rosu doar campul de acum. Ex.: dupa 199 // âCauta" pe formular gol, plecarea aleasa apoi din lista ramanea rosie (alegerea nu emite evenimente). 200 $form.find('input[name="destination"], input[name="departure"], input[name="dates"], .menu-item') 201 .removeClass('searchbox-invalid'); 202 $input.off(ns); 203 $box.off(ns); 204 if (issue.field === 'dates') { 205 // O data care nu mai e plecare a perechii nu ramane afisata: golim tot, ca la o data lipsa. 206 if (issue.stale) resetPackDate(packSearchScope($input.length ? $input[0] : $form[0])); 207 $input.one('change' + ns, unflag); // suggestion-calendar declanseaza change dupa ce scrie .start-at 208 } else { 209 $input.one('input' + ns, unflag);
210 $box.one('click' + ns, '.suggest-destination, .suggest-departure', unflag); 211 } 212 $flagged.addClass('searchbox-invalid'); 213 214 if ($menuItem.length) { // offcanvas mobil: deschidem submeniul (lista / calendarul) 215 $menuItem.trigger('click'); 216 } else if (!$input.length) { 217 return; // PDP: destinatia e fixa, n-are camp de ales 218 } else if (issue.field === 'destination') { 219 $input.trigger('focus').trigger('keyup'); // lista filtrata dupa textul tastat (handlerul de keyup) 220 } else if ($input.is('[data-bs-toggle="dropdown"]') && window.bootstrap && bootstrap.Dropdown) { 221 bootstrap.Dropdown.getOrCreateInstance($input[0]).show(); 222 } else if (issue.field === 'dates' && $input.closest('form').find('.calendar-container').hasClass('d-none')) { 223 $input.trigger('click'); // PDP mobil (#mobileHotelForm): panoul calendarului se comuta la click 224 } 225} 226 227$(document).on('submit', 'form', function (e) { 228 var $form = $(this); 229 var action = ($form.attr('action') || '').toLowerCase(); 230 var isSearchForm = action.indexOf('/search/') !== -1 231 || action.indexOf('/rezultate-cautare-') !== -1; 232 // Validam kids_age pe form-urile care contin in interior selectele 233 // kids_age[] (form-urile de search din pachete/circuite/etc unde 234 // dropdown-ul travellers e RENDERED in <form>). Form-urile cart-add 235 // (AddTourCartForm, AddCartForm) au doar kids hidden â pentru ele 236 // dropdown-ul travellers e in afara form-ului si validarea ar pica 237 // false-positive (kids=1 dar selects=0). Skipam pentru ele. 238 var hasKidsAgeSelects = $form.find('select[name="kids_age[]"]').length > 0; 239 if (!isSearchForm && !hasKidsAgeSelects) return; 240 241 var missing = []; 242 // Pe form-ul de circuite (/search/tours) destinatia + plecarea sunt OPTIONAL â 243 // user-ul poate cauta doar dupa luna + transport (ex: "toate circuitele cu 244 // avion in Iunie"). Backend-ul (FrontendSearchController + Eurosite) trateaza 245 // destinatie goala ca destination_type='all' si itereaza toate tarile. 246 var isToursSearch = action.indexOf('/search/tours') !== -1; 247 if (isSearchForm && !isToursSearch) { 248 var $destination = $form.find('input[name="destination"], input[name="destination_code"]').filter(function () { 249 return $(this).attr('type') !== 'hidden'; 250 }).first(); 251 var $departure = $form.find('input[name="departure"]').filter(function () { 252 return $(this).attr('type') !== 'hidden'; 253 }).first(); 254 255 if ($destination.length && !$.trim($destination.val())) missing.push($destination); 256 if ($departure.length && !$.trim($departure.val())) missing.push($departure); 257 } 258 259 // Verificam kids_age: daca sunt copii dar nu toate varstele sunt alese, 260 // evitam trimiterea unui XML cu <Age></Age> gol catre Eurosite (API-ul 261 // returneaza 0 rezultate si searchul apare ca "nu sunt oferte"). 262 var kidsCount = parseInt($form.find('input[name="kids"]').val(), 10) || 0; 263 var $kidsAgeSelects = $form.find('select[name="kids_age[]"]'); 264 var kidsAgeIncomplete = false; 265 if (kidsCount > 0) { 266 if ($kidsAgeSelects.length !== kidsCount) { 267 kidsAgeIncomplete = true; 268 } else { 269 $kidsAgeSelects.each(function () { 270 if (!$.trim($(this).val())) kidsAgeIncomplete = true; 271 }); 272 } 273 } 274 275 // DEV-577 â Sejururi / 7+7: cu textele completate, verificam ce pleaca de fapt la server 276 // (codurile, lista de plecari, data). Doar /search/packs: la circuite destinatia e optionala, 277 // la zboruri âToate datele" goleste data intentionat (DEV-479), iar cazarea are interval. 278 var packIssue = missing.length === 0 && isPackSearchForm($form) ? packSearchIssue($form) : null; 279 280 if (missing.length === 0 && !kidsAgeIncomplete && !packIssue) return; 281 282 e.preventDefault(); 283 284 $.each(missing, function (_, $el) { 285 $el.addClass('searchbox-invalid'); 286 $el.one('input change focus', function () { 287 $(this).removeClass('searchbox-invalid'); 288 }); 289 }); 290 291 if (kidsAgeIncomplete) { 292 var $travellers = $form.find('input[name="travellers"]').filter(function () { 293 return $(this).attr('type') !== 'hidden'; 294 }).first(); 295 if ($travellers.length) { 296 $travellers.addClass('searchbox-invalid'); 297 $travellers.one('input change focus click', function () { 298 $(this).removeClass('searchbox-invalid'); 299 }); 300 } 301 $kidsAgeSelects.filter(function () { return !$.trim($(this).val()); }) 302 .addClass('searchbox-invalid') 303 .one('change', function () { $(this).removeClass('searchbox-invalid'); }); 304 } 305 306 if (missing.length) { 307 missing[0].focus(); 308 } else if (packIssue) { 309 flagPackSearchIssue($form, packIssue); // DEV-577: marcheaza + deschide lista / calendarul 310 } else if (kidsAgeIncomplete) { 311 var $firstEmpty = $kidsAgeSelects.filter(function () { return !$.trim($(this).val()); }).first(); 312 if ($firstEmpty.length) $firstEmpty.focus(); 313 } 314 315 var msg; 316 if (missing.length > 1) { 317 msg = 'Te rugÄm sÄ completezi destinaÈia Èi plecarea înainte de cÄutare.'; 318 } else if (missing.length === 1) { 319 msg = 'Te rugÄm sÄ alegi o destinaÈie înainte de cÄutare.'; 320 } else if (packIssue) { 321 msg = packIssue.message; 322 } else { 323 msg = kidsCount === 1 324 ? 'Te rugÄm sÄ alegi vârsta copilului înainte de cÄutare.' 325 : 'Te rugÄm sÄ alegi vârsta pentru fiecare copil înainte de cÄutare.'; 326 } 327 searchboxErrorToast(msg); 328}); 329 330// ========================================================================== 331// Ultimul pas al barei declanseaza cautarea (desktop + mobil, toate searchbox-urile). 332// Ordinea pasilor e chiar ordinea campurilor: pe desktop `.flex-grow-1` din `.search-bar`, pe mobil 333// randurile `.menu-item` din meniul principal al tabului. Un camp e âpas" daca mai are ceva de ales: 334// un dropdown sau un select / submeniu de nopti cu > 1 optiune (la zboruri cu âToate datele", noptile 335// nu conteaza: fiecare pereche dus-intors are durata ei). Butonul de confirmare al ultimului pas scrie 336// âCautÄ" si trimite formularul; daca mai urmeaza un pas, ramane âAplicÄ" / âContinuÄ" si duce acolo. 337// Decizia e aici, nu pe partialul de calatori: pe barele cu nopti depinde de data aleasa (noptile se 338// reconstruiesc dupa calendar), iar butoanele âContinuÄ" de pe mobil sunt in fiecare blade. 339// Doar barele de cautare spre listing: PDP-ul (url = pagina produsului) si formularele de cos raman 340// pe âAplicÄ", fara trimitere. 341// ========================================================================== 342function searchbarForm(el) { 343 var $form = $(el).closest('form'); 344 if (!$form.length) return $(); 345 var action = ($form.attr('action') || '').toLowerCase(); 346 if (action.indexOf('/search/') === -1 && action.indexOf('/rezultate-cautare-') === -1) return $(); 347 return packTargetsListing($form) ? $form : $(); 348} 349 350// Zboruri, DEV-479: âToate datele" e o stare, nu o data â eticheta butonului sta in inputul de date. 351function searchbarAnyDate($scope) { 352 var $btn = $scope.find('.flight-any-date-btn').first(); 353 if (!$btn.length) return false;
354 var label = $.trim($btn.text()); 355 return $scope.find('input.dates, input.dates-mobile').filter(function () { 356 return $.trim(this.value || '') === label; 357 }).length > 0; 358} 359 360function searchbarRealOptions($select) { 361 return $select.find('option').filter(function () { return this.value !== ''; }).length; 362} 363 364// Desktop: primul camp de dupa cel curent care mai cere o alegere ($() = campul curent e ultimul). 365function searchbarNextStepDesktop(el) { 366 var $section = $(el).closest('.flex-grow-1'); 367 var $bar = $section.closest('.search-bar'); 368 if (!$bar.length) return $(); 369 var anyDate = searchbarAnyDate($bar); 370 var $next = $(); 371 $section.nextAll('.flex-grow-1').each(function () { 372 var $field = $(this); 373 if ($field.find('[data-bs-toggle="dropdown"]').length) { $next = $field; return false; } 374 var $select = $field.find('select').first(); 375 if ($select.length && !($select.hasClass('nights') && anyDate) && searchbarRealOptions($select) > 1) { 376 $next = $field; 377 return false; 378 } 379 }); 380 return $next; 381} 382 383// Mobil: submeniul urmator din ordinea randurilor meniului principal ($() = submeniul curent e ultimul). 384function searchbarNextStepMobile(el) { 385 var $sub = $(el).closest('.submenu-container'); 386 var id = $sub.attr('id'); 387 if (!id) return $(); 388 var $tab = $sub.closest('.search-type-content'); 389 if (!$tab.length) $tab = $sub.closest('form'); 390 var $items = $tab.find('.menu-item[data-target]'); 391 var idx = $items.index($items.filter('[data-target="' + id + '"]')); 392 if (idx === -1) return $(); 393 var anyDate = searchbarAnyDate($tab); 394 var $next = $(); 395 $items.slice(idx + 1).each(function () { 396 var $target = $tab.find('#' + $(this).attr('data-target')).first(); 397 if (!$target.length) return; 398 if ($target.find('.nights-radio-group').length && 399 (anyDate || $target.find('.nights-radio-label').not('.d-none').length < 2)) return; // nimic de ales 400 $next = $target; 401 return false; 402 }); 403 return $next; 404} 405 406function searchbarNextStep(el) { 407 return $(el).closest('.submenu-container').length ? searchbarNextStepMobile(el) : searchbarNextStepDesktop(el); 408} 409 410function searchbarIsLastStep(el) { 411 return searchbarForm(el).length > 0 && !searchbarNextStep(el).length; 412} 413 414// Desktop: deschide campul urmator. Pe un tick: deschis in acelasi click, dropdown-ul ar fi inchis 415// imediat de handlerul global de click al Bootstrap (capcana de la .flight-any-date-btn, DEV-557). 416// Noptile sunt un <select> nativ: focus + lista deschisa unde browserul o permite (showPicker). 417function openSearchbarStep($field) { 418 var $toggle = $field.find('[data-bs-toggle="dropdown"]').first(); 419 if ($toggle.length) { 420 if (window.bootstrap && bootstrap.Dropdown) { 421 setTimeout(function () { bootstrap.Dropdown.getOrCreateInstance($toggle[0]).show(); }, 0); 422 } 423 return; 424 } 425 var select = $field.find('select').first()[0]; 426 if (!select) return; 427 setTimeout(function () { 428 select.focus(); 429 try { 430 if (typeof select.showPicker === 'function') select.showPicker(); 431 } catch (err) { /* fara suport / fara gestul utilizatorului: ramane focusul */ } 432 }, 0); 433} 434 435// Trimite bara ca un click pe âCautÄ": requestSubmit (sau click pe butonul de submit) emite `submit`, 436// deci ruleaza validarea DEV-577 de mai sus si listenerii nativi (GA4). NU form.submit() â le-ar sari. 437// Pe un tick: daca validarea blocheaza si deschide un dropdown, click-ul curent nu-l mai inchide. 438function submitSearchbar(el) { 439 var $form = searchbarForm(el); 440 if (!$form.length) return false; 441 var form = $form[0]; 442 var $submits = $form.find('button[type="submit"], input[type="submit"]'); 443 var submitter = $submits.filter(':visible').first()[0] || $submits.first()[0]; 444 setTimeout(function () { 445 if (typeof form.requestSubmit === 'function') { 446 if (submitter) form.requestSubmit(submitter); else form.requestSubmit(); 447 } else if (submitter) { 448 submitter.click(); 449 } 450 }, 0); 451 return true; 452} 453 454// âAplicÄ" / âCautÄ" la calatori: nu trecem mai departe cu copii fara varsta aleasa. Marcam select-urile 455// goale si aratam mesajul sub ele; true = blocat. 456function travellersAgesBlocked($tSection) { 457 if (!$tSection.length) return false;
458 var kidsCount = parseInt($tSection.find('.travellers-kids').val(), 10) || 0; 459 var $ageSelects = $tSection.find('select[name="kids_age[]"]'); 460 var $emptyAges = $ageSelects.filter(function () { return !$.trim($(this).val()); }); 461 if (kidsCount === 0 || ($ageSelects.length === kidsCount && $emptyAges.length === 0)) return false; 462 $emptyAges.addClass('is-invalid'); 463 var $ageErr = $tSection.find('.kids-age-error'); 464 if (!$ageErr.length) { 465 $ageErr = $('<div class="kids-age-error text-danger small mt-2"></div>'); 466 $tSection.find('.kids-details').after($ageErr); 467 } 468 $ageErr.text('Te rugam sa alegi varsta pentru fiecare copil.').show(); 469 return true; 470} 471 472// Eticheta butoanelor de confirmare: âCautÄ" pe ultimul pas, altfel textul din markup. Recalculata cand 473// se poate schimba raspunsul (data aleasa reface noptile, tabul 7+7, âToate datele") si la deschidere. 474function syncSearchbarConfirmLabels(scope) { 475 var $root = scope ? $(scope).closest('form') : $(document); 476 if (!$root.length) $root = $(document); 477 $root.find('.travellers-section .dropdown-close, .travellers-continue-btn, .nights-continue-btn').each(function () { 478 var $btn = $(this); 479 if (!searchbarForm(this).length) return; // PDP / cos: neatinse 480 if ($btn.attr('data-label-default') === undefined) $btn.attr('data-label-default', $.trim($btn.text())); 481 $btn.text(searchbarIsLastStep(this) ? 'CautÄ' : $btn.attr('data-label-default')); 482 }); 483} 484$(function () { syncSearchbarConfirmLabels(); }); 485$(document).on('show.bs.dropdown', '.search-bar .travellers-section', function () { 486 syncSearchbarConfirmLabels(this); 487}); 488$(document).on('change', 'input.dates, input.dates-mobile, .search-type-selector', function () { 489 var self = this; 490 setTimeout(function () { syncSearchbarConfirmLabels(self); }, 0); // dupa handlerele care refac noptile 491}); 492$(document).on('click', '.menu-item, .flight-any-date-btn', function () { 493 var self = this; 494 setTimeout(function () { syncSearchbarConfirmLabels(self); }, 0); 495}); 496$(document).on('shown.bs.offcanvas', '.offcanvas', function () { syncSearchbarConfirmLabels(this); }); 497 498// Sejururi pe buget (Data â Nopti â Buget â Oras): orasul e ultimul pas. Alegerea lui poate reface lista de 499// plecari si goli data (handlerele de alegere de mai jos) â fara data nu trimitem, deschidem calendarul. 500// Inregistrat INAINTEA handlerelor de alegere si amanat un tick: vedem starea de dupa ele, chiar daca unul 501// dintre ele arunca (pe buget sugestiile n-au data-departure-dates, iar Object.keys(undefined) arunca). 502$('body').on('click', '.search-bar .suggest-departure, #offcanvasMobile .suggest-departure', function () { 503 var self = this; 504 if (!searchbarIsLastStep(self)) return; 505 setTimeout(function () { 506 var mobile = $(self).closest('.submenu-container').length > 0; 507 var $scope = mobile ? $(self).closest('.search-type-content') : $(self).closest('.search-bar'); 508 var hasDate = $scope.find('input.dates, input.dates-mobile').filter(function () { 509 return $.trim(this.value || '') !== ''; 510 }).length > 0; 511 if (hasDate) { 512 submitSearchbar(self); 513 } else if (mobile) { 514 var calendarId = $scope.find('.submenu-container').has('input.dates, input.dates-mobile').first().attr('id'); 515 if (calendarId) $scope.find('.menu-item[data-target="' + calendarId + '"]').first().trigger('click'); 516 } else { 517 openSearchbarStep($scope.find('.calendar-section').first()); 518 } 519 }, 0); 520}); 521 522// Returneaza URL-ul suggest corect pentru pack (destination-zone sau departure-city) 523// in functie de context. Daca suntem in 7+7 (desktop: searchBar are clasa .search-7plus7, 524// mobil: .search-type-selector e pe "7plus7"), returneaza varianta -7plus7 filtrata. 525function packSuggestUrl(baseType, contextEl) { 526 var is7plus7 = false; 527 if (contextEl && contextEl.length) { 528 if (contextEl.hasClass && contextEl.hasClass('search-7plus7')) { 529 is7plus7 = true; 530 } else if (contextEl.closest && contextEl.closest('.search-7plus7').length) { 531 is7plus7 = true; 532 } 533 } 534 if (!is7plus7 && $('.search-type-selector').val() === '7plus7') { 535 is7plus7 = true; 536 } 537 return '/suggest/pack-' + baseType + (is7plus7 ? '-7plus7' : ''); 538} 539 540$(function () { 541 (function () { 542 const PLACEHOLDER_BASE = "https://paralela45.ro/dir/fundal"; 543 const PLACEHOLDER_COUNT = 10; 544 545 const randomPlaceholder = () => { 546 const n = Math.floor(Math.random() * PLACEHOLDER_COUNT) + 1; 547 return `${PLACEHOLDER_BASE}/${n}.jpg?v=3`; 548 }; 549 550 const applyFallback = (img) => { 551 if (img.dataset.fallbackApplied === "1") return; 552 img.dataset.fallbackApplied = "1"; 553 img.src = randomPlaceholder(); 554 555 // Show the hidden image column 556 const colImage = img.closest(".col-image"); 557 if (colImage) { 558 colImage.classList.remove("d-none"); 559 } 560 }; 561 562 const prepareImg = (img) => { 563 if (img.dataset.fallbackApplied === "1") return; 564 565 const attrSrc = (img.getAttribute("src") || "").trim(); 566 567 // Empty src â fallback immediately 568 if (!attrSrc || attrSrc === "#" || attrSrc === "about:blank") { 569 applyFallback(img); 570 return; 571 } 572 573 // Fix mixed content 574 if (img.src && img.src.startsWith("http://")) { 575 img.src = img.src.replace(/^http:\/\//i, "https://"); 576 } 577 578 // Already finished loading but broken â fallback
579 if (img.complete && img.naturalWidth === 0) { 580 applyFallback(img); 581 return; 582 } 583 584 // Bind error listener (only once) 585 if (img.dataset.fallbackBound !== "1") { 586 img.dataset.fallbackBound = "1"; 587 img.addEventListener("error", () => applyFallback(img), { once: true }); 588 } 589 }; 590 591 const processAllImages = () => { 592 document.querySelectorAll(".hotels .hotel-img").forEach(prepareImg); 593 }; 594 595 // 1. Process any images already in DOM 596 processAllImages(); 597 598 // 2. Re-scan after every jQuery AJAX completes (catches update_prices responses) 599 $(document).ajaxComplete(function () { 600 setTimeout(processAllImages, 50); 601 setTimeout(processAllImages, 500); 602 }); 603 604 // 3. Global error capture as safety net 605 document.addEventListener("error", (e) => { 606 const img = e.target; 607 if (!(img instanceof HTMLImageElement)) return; 608 if (!img.closest(".hotels")) return; 609 applyFallback(img); 610 }, true); 611 })(); 612}); 613// For Desktop 614const destinationDropdown = $('.dropdown'); 615$(destinationDropdown).on('hide.bs.dropdown', function (event) { 616 searchBar = $(this).closest('.search-bar'); 617 if (searchBar.find('.dropdown-menu.show').length == 1) { 618 searchBar.removeClass('square-corner-start'); 619 searchBar.removeClass('square-corner-end'); 620 } 621}); 622$(destinationDropdown).on('show.bs.dropdown', function (event) { 623 $('.dropdown-menu').dropdown('hide'); 624 searchBar = $(this).closest('.search-bar'); 625 searchBar.removeClass('square-corner-start'); 626 searchBar.removeClass('square-corner-end'); 627 if ($(this).hasClass('start')) { 628 searchBar.addClass('square-corner-start'); 629 } else if ($(this).hasClass('end')) { 630 searchBar.addClass('square-corner-end'); 631 } 632 633 if (!isTyping) { 634 input = $(this).find('.form-control'); 635 if (input.hasClass('suggest-pack-destination-zone')) { 636 dropdown = $(this); 637 terms = { 638 q: '' 639 }; 640 if (destination_is_departure) { 641 terms.departure_city_code = searchBar.find('.departure-code').val(); 642 } 643 city_suggestion(packSuggestUrl('destination-zone', searchBar), terms, dropdown); 644 } else if (input.hasClass('suggest-pack-departure-city')) { 645 dropdown = $(this); 646 terms = { 647 q: '' 648 }; 649 if (!destination_is_departure) { 650 terms.destination_code = searchBar.find('.destination-code').val(); 651 } 652 city_suggestion(packSuggestUrl('departure-city', searchBar), terms, dropdown); 653 } else if (input.hasClass('suggest-charter-destination-zone')) { 654 dropdown = $(this); 655 terms = { q: '' }; 656 if (destination_is_departure) { 657 terms.departure_city_code = searchBar.find('.departure-code').val(); 658 } 659 city_suggestion("/suggest/charter-destination-zone", terms, dropdown); 660 } else if (input.hasClass('suggest-charter-departure-city')) { 661 dropdown = $(this); 662 terms = { q: '' }; 663 if (!destination_is_departure) { 664 terms.destination_code = searchBar.find('.destination-code').val(); 665 } 666 city_suggestion("/suggest/charter-departure-city", terms, dropdown); 667 } 668 } 669}); 670$('body').on('click', function (e) { 671 if (!$(e.target).closest('.dropdown').length) { 672 $('.dropdown-menu').dropdown('hide'); 673 } 674}); 675$('body').on('click', '.dropdown-close', function (e) { 676 e.preventDefault(); 677 // "Aplica": nu permitem aplicarea daca sunt copii fara varsta aleasa. 678 // Marcam select-urile goale si aratam mesaj sub ele; NU inchidem dropdown-ul 679 // (stopPropagation ca sa nu declanseze nici refresh-ul de camere). 680 if (travellersAgesBlocked($(this).closest('.travellers-section'))) { 681 e.stopPropagation(); 682 return; 683 } 684 // ActualizeazÄ inputul de cÄlÄtori înainte de a închide 685 var travellersWrap = $(this).closest('.travellers-wrap'); 686 if (travellersWrap.length) { 687 calcTotalTravellers(travellersWrap); 688 } 689 var dropdownMenu = $(this).closest('.dropdown-menu'); 690 var dropdownToggle = dropdownMenu.siblings('[data-bs-toggle="dropdown"]'); 691 if (dropdownToggle.length) { 692 var bsDropdown = bootstrap.Dropdown.getOrCreateInstance(dropdownToggle[0]); 693 bsDropdown.hide(); 694 } else { 695 dropdownMenu.removeClass('show'); 696 } 697 698 // Bara de cautare: pe ultimul pas (âCautÄ") trimitem; pe desktop, daca mai urmeaza un pas (noptile
699 // la Sejururi / 7+7 / Bilete), il deschidem. PDP-ul si cosul nu intra aici (searchbarForm gol). 700 if (searchbarForm(this).length) { 701 var $nextStep = searchbarNextStep(this); 702 if (!$nextStep.length) { 703 submitSearchbar(this); 704 } else if (!$(this).closest('.submenu-container').length) { 705 openSearchbarStep($nextStep); 706 } 707 } 708}); 709 710// La alegerea unei varste, curatam starea de eroare (border rosu + mesaj) daca 711// toate varstele din sectiune au fost completate. 712$('body').on('change', 'select[name="kids_age[]"]', function () { 713 var $sel = $(this); 714 if ($.trim($sel.val())) $sel.removeClass('is-invalid'); 715 var $sec = $sel.closest('.travellers-section'); 716 if ($sec.length) { 717 var $stillEmpty = $sec.find('select[name="kids_age[]"]').filter(function () { return !$.trim($(this).val()); }); 718 if (!$stillEmpty.length) $sec.find('.kids-age-error').hide(); 719 } 720}); 721 722function city_suggestion(url, terms, container) { 723 $.ajax({ 724 url: url, 725 type: 'POST', 726 async: true, 727 cache: false, 728 headers: { 'X-Update-Token': window._updateToken || '' }, 729 data: terms, 730 success: function (data) { 731 container.find('.dropdown-menu').html(data); 732 }, 733 beforeSend: function () { 734 container.find('.dropdown-menu').html( 735 '<div class="text-center py-3"><i class="spinner-border spinner-border-sm"></i></div>' 736 ); 737 }, 738 error: function () { 739 container.find('.dropdown-menu').html( 740 '<div class="text-center py-3">Nu merg sugestiile.</div>'); 741 notification("Nu merg sugestiile.", "danger"); 742 } 743 }); 744} 745function next_dropdown(section) { 746 let nextDropdown = section.nextAll('.flex-grow-1').find('.dropdown').first(); 747 if (nextDropdown.length == 1) { 748 let myDropdown = 749 new bootstrap.Dropdown(nextDropdown, { 750 autoClose: true, 751 display: 'static' 752 }); 753 myDropdown.toggle(); 754 } 755} 756var tourValidRangeCache = undefined; // undefined=not fetched, null=fetch error/no range, array=fetched 757var tourMonthPickerInstance = null; 758var tourPickerFetching = false; 759 760function getDefaultTourMonth() { 761 var now = new Date(); 762 var next = new Date(now.getFullYear(), now.getMonth() + 1, 1); 763 var shortMonths = ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 764 return shortMonths[next.getMonth()] + ' ' + next.getFullYear(); 765} 766 767// jSuites value option expects ISO "YYYY-MM-DD", not display format "MMM YYYY" 768function tourMonthDisplayToISO(displayVal) { 769 var shortMonths = ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 770 var parts = String(displayVal || '').trim().split(' '); 771 if (parts.length !== 2) return null; 772 var monthIdx = shortMonths.indexOf(parts[0]); 773 if (monthIdx === -1) return null; 774 return parts[1] + '-' + String(monthIdx + 1).padStart(2, '0') + '-01'; 775} 776 777// Circuite: dupa luna (sau âToate datele") deschidem campul urmator din bara â Calatori; pe 778// searchbox-urile pe buget, Bugetul â, ca la Sejururi dupa alegerea zilei (suggestion-calendar). 779// Nimic nu devine obligatoriu: doar ghidam. Ordinea pasilor: searchbarNextStepDesktop. 780function openTourNextField(el) { 781 var $next = searchbarNextStepDesktop(el); 782 if ($next.length) openSearchbarStep($next); 783} 784 785// Circuite (mobil): dupa luna / âToate datele" trecem la submeniul urmator din meniul principal 786// (Calatori; pe buget, Bugetul), ca la Sejururi dupa data (handlerul de change pe .dates). Daca nu 787// mai urmeaza nimic, revenim la meniul principal, ca inainte. 788function goToNextTourSubmenu($offcanvas, currentMenu, mainMenu) { 789 var $current = $offcanvas.find(currentMenu).first(); 790 var $next = $current.length ? searchbarNextStepMobile($current[0]) : $(); 791 if ($next.length) { 792 $offcanvas.find('.submenu-container').removeClass('active'); 793 $next.addClass('active'); 794 return; 795 } 796 var backBtn = $current.find('.back-btn').first(); 797 if (backBtn.length) backBtn.trigger('click'); 798 else { 799 $current.removeClass('active'); 800 $offcanvas.find(mainMenu).removeClass('hidden'); 801 } 802} 803 804function openTourMonthPickerWithRange(validRange) { 805 var el = document.getElementById('monthpicker'); 806 if (!el) return; 807 // Destroy stale instance so it doesn't retain a previously set validRange 808 try { 809 if (tourMonthPickerInstance && typeof tourMonthPickerInstance.destroy === 'function') { 810 tourMonthPickerInstance.destroy(); 811 } else if (el.calendar && typeof el.calendar.destroy === 'function') { 812 el.calendar.destroy(); 813 } 814 } catch (e) {} 815 tourMonthPickerInstance = null; 816 var displayVal = el.value || getDefaultTourMonth(); 817 var isoVal = tourMonthDisplayToISO(displayVal); 818 var opts = { 819 type: 'year-month-picker', 820 value: isoVal, 821 format: 'MMM YYYY',
822 resetButton: false, 823 placeholder: 'Alege o luna', 824 months: ['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 825 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie' 826 ] 827 }; 828 // Always pass validRange when server returned an array (even [] disables all months) 829 // null means AJAX error â fall back to allowing all months 830 if (Array.isArray(validRange)) opts.validRange = validRange; 831 tourMonthPickerInstance = jSuites.calendar(el, opts); 832 833 // DEV-542 â calendarul de circuite: in header ramane doar anul, iar luna 834 // aleasa e evidentiata doar pe anul ei. jSuites marcheaza in getMonths() 835 // luna din date[1] indiferent de anul afisat, asa ca dupa sageata 836 // (2026 -> 2027) âOctombrie" ramanea preselectat si pe 2027. 837 var calBox = el.nextElementSibling; // jSuites insereaza .jcalendar imediat dupa input 838 if (calBox && calBox.classList.contains('jcalendar')) { 839 calBox.classList.add('tour-monthpicker'); // scope pentru regula din devteam.css 840 var inst = tourMonthPickerInstance; // nu variabila globala: âToate datele" o face null 841 var origGetMonths = inst.getMonths; 842 inst.getMonths = function () { 843 origGetMonths(); 844 // el.value e deja actualizat de setValue() inainte sa ajunga aici 845 var iso = tourMonthDisplayToISO(el.value); 846 var selYear = iso ? parseInt(iso.slice(0, 4), 10) : null; 847 if (parseInt(inst.date[0], 10) !== selYear) { 848 calBox.querySelectorAll('td.jcalendar-set-month.jcalendar-selected').forEach(function (td) { 849 td.classList.remove('jcalendar-selected'); 850 }); 851 } 852 }; 853 } 854 855 // Circuite: luna aleasa -> campul urmator. jSuites n-are un eveniment doar pentru alegere 856 // (onchange vine si la init / la inchiderea fara alegere, onclose si la click in afara), asa ca 857 // prindem update(), apelat numai la click pe o luna (pe mouseup sau touchend). 858 var pickInst = tourMonthPickerInstance; 859 var origUpdate = pickInst.update; 860 pickInst.update = function (element, v) { 861 var picked = !!element && element.classList.contains('jcalendar-set-month') && 862 !element.classList.contains('jcalendar-disabled'); 863 origUpdate(element, v); 864 if (picked) openTourNextField(el); 865 }; 866 tourMonthPickerInstance.open(); 867 868 // Inject "Toate datele" button below the jSuites calendar popup. 869 // Wrapper cu padding ca butonul sa nu iasa din dropdown si distantarea 870 // sa fie controlata de container, nu de calc(100% - X) pe buton. 871 setTimeout(function () { 872 var popup = document.querySelector('.jcalendar-content'); 873 if (popup && !popup.querySelector('.tour-all-dates-btn-wrap')) { 874 var wrap = document.createElement('div'); 875 wrap.className = 'tour-all-dates-btn-wrap'; 876 wrap.style.cssText = 'padding: 16px 14px 14px 14px; box-sizing: border-box;'; 877 878 var btn = document.createElement('button'); 879 btn.type = 'button'; 880 btn.className = 'tour-all-dates-btn btn btn-outline-primary w-100 rounded-pill fw-semibold'; 881 btn.textContent = 'Toate datele'; 882 btn.style.cssText = 'padding: 12px 16px; font-size: 14px; position: relative; z-index: 5;'; 883 884 // Stop propagation pe mousedown ca jSuites sa nu interpreteze click-ul 885 // drept "click in afara popup-ului" si sa apeleze close. 886 btn.addEventListener('mousedown', function (ev) { ev.stopPropagation(); }); 887 btn.addEventListener('click', function (ev) { 888 ev.preventDefault(); 889 ev.stopPropagation(); 890 891 // Setam valoarea direct pe input. 892 el.value = 'Toate datele'; 893 894 // Inchidem popup-ul vizual. 895 document.querySelectorAll('.jcalendar.jcalendar-focus').forEach(function (c) { 896 c.classList.remove('jcalendar-focus'); 897 }); 898 899 tourMonthPickerInstance = null; 900 901 $(el).trigger('change'); 902 el.blur(); 903 openTourNextField(el); // Circuite: ca dupa alegerea unei luni 904 905 // Reasezam valoarea dupa next tick pentru cazul in care jSuites 906 // o suprascrie async (mouseup global, blur handler, etc). 907 setTimeout(function () { 908 if (el.value !== 'Toate datele') { 909 el.value = 'Toate datele'; 910 } 911 }, 50); 912 }); 913 914 wrap.appendChild(btn); 915 popup.appendChild(wrap); 916 } 917 }, 50); 918} 919 920function initializeTourMonthPicker(destinationCode, open = true) { 921 var openMonthPicker = open; 922 // Close previous instance if any 923 try { 924 if (tourMonthPickerInstance && typeof tourMonthPickerInstance.close === 'function') { 925 tourMonthPickerInstance.close(); 926 } 927 } catch (e) {} 928 if (openMonthPicker) tourMonthPickerInstance = null; 929 tourPickerFetching = true; 930 $.ajax({ 931 url: "/suggest/tour-dates", 932 type: 'POST', 933 async: true, 934 cache: false,
935 headers: { 'X-Update-Token': window._updateToken || '' }, 936 data: { 937 destination_code: destinationCode 938 }, 939 success: function (data) { 940 tourPickerFetching = false; 941 var dates = null; 942 try { dates = JSON.parse(data); } catch (e) {} 943 tourValidRangeCache = dates; 944 if (openMonthPicker) { 945 openTourMonthPickerWithRange(dates); 946 } 947 // open=false: validRange stored, jSuites NOT initialized yet (lazy init on click) 948 }, 949 error: function () { 950 tourPickerFetching = false; 951 tourValidRangeCache = null; 952 if (openMonthPicker) { 953 openTourMonthPickerWithRange(null); 954 } 955 // open=false: validRange stored as null, jSuites NOT initialized yet (lazy init on click) 956 } 957 }); 958} 959 960// Lazy initialization: initialize and open monthpicker on first click 961// This prevents flash caused by jSuites overwriting the PHP-rendered session value on page load 962$(document).on('click', '.search-tours .calendar-section', function () { 963 if (tourMonthPickerInstance) return; // already initialized, jSuites handles it 964 if (tourPickerFetching) return; // AJAX in progress â wait for it to complete 965 // Budget mode: search bar has pre-loaded dates via data-tour-dates (no AJAX destination step) 966 var preloadedDates = $(this).closest('.search-bar').data('tour-dates'); 967 if (preloadedDates !== undefined) { 968 openTourMonthPickerWithRange(Array.isArray(preloadedDates) ? preloadedDates : null); 969 return; 970 } 971 var range = tourValidRangeCache !== undefined ? tourValidRangeCache : null; 972 openTourMonthPickerWithRange(range); 973}); 974 975tour_destination_code = $('.search-tours .destination-code').val(); 976if (tour_destination_code != undefined && tour_destination_code != '') { 977 initializeTourMonthPicker(tour_destination_code, false); 978} 979 980// DEV-577 â textul tastat peste o alegere nu mai pleaca cu codul ei (Cancun -> âOman" ramanea CNC): 981// la prima tasta golim codul si tot ce depindea de pereche (lista de plecari, data). Pe `input`, nu 982// pe keyup: prinde si lipirea / stergerea din meniul contextual, iar sagetile / Tab nu golesc nimic. 983// Click-ul pe o sugestie scrie prin .val(), care nu emite `input`, deci alegerea ramane intreaga. 984$('body').on('input', '.suggest-pack-destination-zone, .suggest-pack-destination-zone-mobile', function () { 985 if (!isPackSearchForm($(this).closest('form'))) return; 986 var $scope = packSearchScope(this); 987 $scope.find('.destination-code').val(''); 988 $scope.find('.destination-preview').text('Alege destinaÈia'); // mobil: randul din meniul principal 989 resetPackPair($scope); 990}); 991$('body').on('input', '.suggest-pack-departure-city, .suggest-pack-departure-city-mobile', function () { 992 if (!isPackSearchForm($(this).closest('form'))) return; 993 var $scope = packSearchScope(this); 994 $scope.find('.departure-code').val(''); 995 $scope.find('.departure-preview').text('Alege oraÈul'); 996 resetPackPair($scope); 997}); 998 999$('body').on('keyup', '.suggest-pack-destination-zone', function () { 1000 clearTimeout(suggestionTimer); 1001 var self = this; 1002 suggestionTimer = setTimeout(function () { 1003 searchBar = $(self).closest('.search-bar'); 1004 dropdown = $(self).closest('.dropdown'); 1005 mobile_container = $('#destinationMenu'); 1006 terms = { 1007 q: $(self).val() 1008 }; 1009 if (destination_is_departure) { 1010 terms.departure_city_code = searchBar.find('.departure-code').val(); 1011 } 1012 var destUrl = searchBar.hasClass('search-bilete') ? "/suggest/charter-destination-zone" : packSuggestUrl('destination-zone', searchBar); 1013 if (!$(self).hasClass('show')) { 1014 isTyping = true; 1015 let myDropdown = 1016 new bootstrap.Dropdown(dropdown, { 1017 autoClose: true, 1018 display: 'static' 1019 }); 1020 myDropdown.show(); 1021 isTyping = false; 1022 } 1023 city_suggestion(destUrl, terms, dropdown); 1024 city_suggestion_mobile(destUrl, terms, mobile_container); 1025 }, 300); 1026}); 1027$('body').on('click', '.search-bar .suggest-destination', function () { 1028 section = $(this).closest('.destination-section'); 1029 section.find('.destination').val($(this).data('destination')); 1030 section.find('.destination-code').val($(this).data('destination-code')); 1031 searchBar = $(this).closest('.search-bar'); 1032
1033 let currentDropdown = $(this).closest('.dropdown'); 1034 currentDropdown.find('input').first().dropdown('hide'); 1035 1036 if (destination_is_departure) { 1037 next_dropdown(section); 1038 1039 dates = $(this).data('departure-dates'); 1040 searchBar.attr('data-departure-dates', JSON.stringify(dates)); 1041 searchBar.find('.departure-dates').val(JSON.stringify(dates)); 1042 picker = window[searchBar.data('picker')]; 1043 dates = Object.keys(dates); 1044 1045 if (dates && dates.length > 0) { 1046 const dateObj = new Date(dates[0]); 1047 const day = dateObj.getDate().toString().padStart(2, '0'); 1048 const month = dateObj.toLocaleString("en-US", { 1049 month: "short" 1050 }); 1051 const year = dateObj.getFullYear(); 1052 1053 // Format as "DD MMM YYYY" 1054 const formattedDate = `${day} ${month} ${year}`; 1055 picker.gotoDate(formattedDate); 1056 } 1057 searchBar.find('.dates').val(''); 1058 searchBar.find('.start-at, .end-at').val(''); // DEV-577: data perechii vechi nu mai pleaca ascunsa 1059 searchBar.find('.calendar-preview').text('Alege datele'); 1060 picker.renderAll(); 1061 } else if (section.find('.destination').hasClass('suggest-tour-destination-city')) { 1062 initializeTourMonthPicker($(this).data('destination-code')); 1063 } else { 1064 if (searchBar.hasClass('search-packs')) { 1065 searchBar.attr('data-departure-dates', JSON.stringify('[]')); 1066 searchBar.find('.departure-dates').val(JSON.stringify('[]')); 1067 searchBar.find('.dates').val(''); 1068 searchBar.find('.start-at, .end-at').val(''); // DEV-577 1069 } 1070 1071 next_dropdown(section); 1072 1073 dropdown = searchBar.find('.departure-section .dropdown'); 1074 terms = { 1075 q: '', 1076 destination_code: $(this).data('destination-code') 1077 }; 1078 if (searchBar.hasClass('search-bilete')) { 1079 city_suggestion("/suggest/charter-departure-city", terms, dropdown); 1080 } else { 1081 city_suggestion(packSuggestUrl('departure-city', searchBar), terms, dropdown); 1082 } 1083 } 1084 1085 1086}); 1087$('body').on('keyup', '.suggest-pack-departure-city', function () { 1088 clearTimeout(suggestionTimer); 1089 var self = this; 1090 suggestionTimer = setTimeout(function () { 1091 dropdown = $(self).closest('.dropdown'); 1092 searchBar = $(self).closest('.search-bar'); 1093 terms = { 1094 q: $(self).val() 1095 }; 1096 if (!destination_is_departure) { 1097 terms.destination_code = searchBar.find('.destination-code').val(); 1098 } 1099 var depUrl = searchBar.hasClass('search-bilete') ? "/suggest/charter-departure-city" : packSuggestUrl('departure-city', searchBar); 1100 if (!$(self).hasClass('show')) { 1101 isTyping = true; 1102 let myDropdown = 1103 new bootstrap.Dropdown(dropdown, { 1104 autoClose: true, 1105 display: 'static' 1106 }); 1107 myDropdown.show(); 1108 isTyping = false; 1109 } 1110 city_suggestion(depUrl, terms, dropdown); 1111 }, 300); 1112}); 1113$('body').on('click', '.search-bar .suggest-departure', function () { 1114 section = $(this).closest('.departure-section'); 1115 section.find('.departure').val($(this).data('departure') + ', ' + $(this).data('departure-country')); 1116 section.find('.departure-code').val($(this).data('departure-code')); 1117 searchBar = $(this).closest('.search-bar'); 1118 1119 let currentDropdown = $(this).closest('.dropdown'); 1120 currentDropdown.find('input').first().dropdown('hide'); 1121 next_dropdown(section); 1122 1123 if (destination_is_departure == false) { 1124 dates = $(this).data('departure-dates'); 1125 searchBar.attr('data-departure-dates', JSON.stringify(dates)); 1126 searchBar.find('.departure-dates').val(JSON.stringify(dates)); 1127 picker = window[searchBar.data('picker')]; 1128 dates = Object.keys(dates); 1129 1130 if (dates && dates.length > 0) { 1131 const dateObj = new Date(dates[0]); 1132 const day = dateObj.getDate().toString().padStart(2, '0');
1133 const month = dateObj.toLocaleString("en-US", { 1134 month: "short" 1135 }); 1136 const year = dateObj.getFullYear(); 1137 1138 // Format as "DD MMM YYYY" 1139 const formattedDate = `${day} ${month} ${year}`; 1140 picker.gotoDate(formattedDate); 1141 } 1142 searchBar.find('.dates').val(''); 1143 searchBar.find('.start-at, .end-at').val(''); // DEV-577: data perechii vechi nu mai pleaca ascunsa 1144 searchBar.find('.calendar-preview').text('Alege datele'); 1145 picker.renderAll(); 1146 } else { 1147 if (searchBar.hasClass('search-packs')) { 1148 searchBar.attr('data-departure-dates', JSON.stringify('[]')); 1149 searchBar.find('.departure-dates').val(JSON.stringify('[]')); 1150 searchBar.find('.dates').val(''); 1151 searchBar.find('.start-at, .end-at').val(''); // DEV-577 1152 } 1153 1154 dropdown = searchBar.find('.destintation-section .dropdown'); 1155 terms = { 1156 q: '', 1157 departure_city_code: $(this).data('departure-code') 1158 }; 1159 if (searchBar.hasClass('search-bilete')) { 1160 city_suggestion("/suggest/charter-destination-zone", terms, dropdown); 1161 } else { 1162 city_suggestion(packSuggestUrl('destination-zone', searchBar), terms, dropdown); 1163 } 1164 } 1165}); 1166$('body').on('click', '.btn-switch', function (e) { 1167 e.preventDefault(); // Previne comportamentul implicit al link-ului 1168 1169 // SelectÄm cele douÄ div-uri 1170 var destinationSection = $(this).prevAll('.flex-grow-1').first(); 1171 var departureSection = $(this).nextAll('.flex-grow-1').first(); 1172 1173 if (destinationSection.length && departureSection.length) { 1174 // SchimbÄm poziÈiile între cele douÄ div-uri 1175 var destinationClone = destinationSection.clone(true); 1176 var departureClone = departureSection.clone(true); 1177 1178 // Ãnlocuim destinaÈia cu plecarea Èi invers 1179 destinationSection.replaceWith(departureClone); 1180 departureSection.replaceWith(destinationClone); 1181 1182 city_suggestion(packSuggestUrl('destination-zone', destinationClone), "", destinationClone); 1183 city_suggestion(packSuggestUrl('departure-city', departureClone), "", departureClone); 1184 if (destination_is_departure) { 1185 destination_is_departure = false; 1186 } else { 1187 destination_is_departure = true; 1188 } 1189 } 1190}); 1191 1192// DEV-574 â coloana âCele mai populare" dupa navigarea prin lista (click pe tara / zona / Inapoi), desktop 1193// si mobil (#offcanvasMobile). Aceeasi regula ca la cautarea tastata (App\Support\DestinationTermMatch:: 1194// showPopulars), aplicata pe ce se vede acum: coloana ramane doar daca lista are >= 2 randuri vizibile SI 1195// macar o populara vizibila; altfel dispare, iar lista trece pe toata latimea (p45-dest-suggest--list-only, 1196// fara h-100 / bordura dreapta â exact ca in Blade). Ex.: Mexic -> doar Cancun, nicio populara => fara 1197// coloana (inainte ramanea titlul singur, fara carduri); tara cu o singura destinatie, chiar populara => la fel. 1198// Lista de tari (nivelul de sus, unde duce Inapoi) = starea randata de server, deci coloana revine. 1199// Fara coloana in DOM (serverul a scos-o dupa tastare) nu face nimic: nu reafiseaza ce n-a randat serverul. 1200// Vizibilitatea se citeste din d-none (handlerele comuta d-none / d-flex / d-block), nu din :visible, care 1201// depinde de starea meniului (inchis / offcanvas in animatie), nu de ce a ales utilizatorul. 1202// Tot aici lista revine sus la fiecare schimbare de nivel: altfel pastra scroll-ul de la nivelul anterior 1203// (ai derulat pana la Grecia) si prima destinatie iesea taiata sub âInapoi". 1204function syncDestinationPopulars(container) { 1205 container.find('.p45-dest-suggest').each(function () { 1206 var row = $(this); 1207 var list = row.find('.p45-dest-suggest__list > .list-group'); 1208 list.scrollTop(0); 1209 var column = row.children('.p45-dest-suggest__popular'); 1210 if (!column.length) { 1211 return; 1212 } 1213 var visibleRows = list.children('li').not('.d-none'); 1214 var show = visibleRows.filter('.suggest-destination-country').length > 0 1215 || (visibleRows.length > 1 && column.find('.suggest-destination-popular').not('.d-none').length > 0); 1216 1217 // h-100 + bordura dreapta exista doar langa coloana â le tinem minte la prima trecere, ca Inapoi sa le 1218 // puna la loc exact cum le-a randat partialul (azi toate 4 au border-md-end; border-end ramane in 1219 // filtru ca helperul sa nu depinda de asta) 1220 if (list.attr('data-p45-side-classes') === undefined) { 1221 list.attr('data-p45-side-classes', ['h-100', 'border-end', 'border-md-end'].filter(function (cls) { 1222 return list.hasClass(cls); 1223 }).join(' ')); 1224 } 1225 column.toggleClass('d-none', !show); 1226 row.toggleClass('p45-dest-suggest--list-only', !show); 1227 list.toggleClass(list.attr('data-p45-side-classes'), show); 1228 }); 1229} 1230 1231$('body').on('click', '.search-bar .suggest-destination-country', function (e) { 1232 e.preventDefault(); 1233 dropdown = $(this).closest('.dropdown'); 1234 dropdown.find('.suggest-destination-country').removeClass('d-flex').addClass('d-none'); 1235 dropdown.find('.suggest-destination-popular').removeClass('d-flex').addClass('d-none'); 1236 dropdown.find('.suggest-destination-' + $(this).data('country')).removeClass('d-none').addClass( 1237 'd-flex'); 1238 dropdown.find('.suggest-destination-popular.suggest-destination-' + $(this).data('country')) 1239 .removeClass('d-flex').addClass('d-block'); 1240 dropdown.find('.btn-back').removeClass('d-none').addClass('d-flex'); 1241 syncDestinationPopulars(dropdown); // DEV-574 1242}); 1243$('body').on('click', '.search-bar .suggest-destination-zone', function (e) { 1244 e.preventDefault(); 1245 dropdown = $(this).closest('.dropdown'); 1246 dropdown.find('.suggest-destination-country').removeClass('d-flex').addClass('d-none'); 1247 dropdown.find('.suggest-destination-zone').removeClass('d-flex').addClass('d-none'); 1248 dropdown.find('.suggest-destination-popular').removeClass('d-flex').addClass('d-none'); 1249 dropdown.find('.suggest-destination-' + $(this).data('zone')).removeClass('d-none').addClass( 1250 'd-flex'); 1251 dropdown.find('.suggest-destination-popular.suggest-destination-' + $(this).data('zone')) 1252 .removeClass('d-flex').addClass('d-block'); 1253 dropdown.find('.btn-back').removeClass('d-none').addClass('d-flex'); 1254 syncDestinationPopulars(dropdown); // DEV-574 1255}); 1256$('body').on('click', '.search-bar .btn-back', function (e) { 1257 e.preventDefault(); 1258 dropdown = $(this).closest('.dropdown'); 1259 var backToCountry = null; // DEV-574 1260 if (dropdown.find('.suggest-destination-zone').length) { 1261 suggest_destination = dropdown.find('.col-md-4 .suggest-destination.d-flex').first(); 1262 if (suggest_destination.hasClass('d-flex')) { 1263 backToCountry = suggest_destination.data('country'); 1264 dropdown.find('.suggest-destination-' + backToCountry).removeClass('d-none').addClass('d-flex'); 1265 } else { 1266 dropdown.find('.suggest-destination-zone').removeClass('d-flex').addClass('d-none'); 1267 dropdown.find('.suggest-destination-country').removeClass('d-none').addClass('d-flex'); 1268 dropdown.find('.btn-back').removeClass('d-flex').addClass('d-none'); 1269 } 1270 } else { 1271 dropdown.find('.suggest-destination-country').removeClass('d-none').addClass('d-flex'); 1272 dropdown.find('.btn-back').removeClass('d-flex').addClass('d-none'); 1273 } 1274 dropdown.find('.col-md-4 .suggest-destination').removeClass('d-flex').addClass('d-none'); 1275 dropdown.find('.suggest-destination-popular').removeClass('d-block').removeClass('d-flex').removeClass('d-none'); 1276 // DEV-574 â inapoi din orase la zonele unei tari (Cazare): popularele filtrate ca la click pe tara, nu 1277 // toate â altfel acelasi nivel ar avea sau n-ar avea coloana, dupa cum ai ajuns la el 1278 if (backToCountry) { 1279 dropdown.find('.suggest-destination-popular').addClass('d-none'); 1280 dropdown.find('.suggest-destination-popular.suggest-destination-' + backToCountry) 1281 .removeClass('d-none').addClass('d-block'); 1282 } 1283 syncDestinationPopulars(dropdown); // DEV-574 1284}); 1285 1286$('body').on('change', '.suggest-tour-transport', function () { 1287 section = $(this).closest('.transport-section'); 1288 searchBar = $(this).closest('.search-bar'); 1289 dropdown = searchBar.find('.destination-section .dropdown'); 1290 terms = { 1291 transport: $(this).val(), 1292 q: '', 1293 }; 1294 city_suggestion("/suggest/tour-destination-city", terms, dropdown); 1295 1296 let nextDropdown = section.nextAll('.flex-grow-1').find('.dropdown').first(); 1297 if (nextDropdown.length == 1) { 1298 let myDropdown = 1299 new bootstrap.Dropdown(nextDropdown, { 1300 autoClose: true, 1301 display: 'static' 1302 }); 1303 myDropdown.toggle(); 1304 } 1305}); 1306$('body').on('keyup', '.suggest-tour-destination-city', function () { 1307 clearTimeout(suggestionTimer); 1308 var self = this; 1309 suggestionTimer = setTimeout(function () { 1310 dropdown = $(self).closest('.dropdown'); 1311 terms = { 1312 q: $(self).val(), 1313 transport: $(self).closest('form, .searchbox').find('.suggest-tour-transport').val() || '' 1314 }; 1315 if (!$(self).hasClass('show')) { 1316 isTyping = true; 1317 let myDropdown = 1318 new bootstrap.Dropdown(dropdown, { 1319 autoClose: true, 1320 display: 'static' 1321 }); 1322 myDropdown.show(); 1323 isTyping = false;
1324 } 1325 city_suggestion("/suggest/tour-destination-city", terms, dropdown); 1326 }, 300); 1327}); 1328$('body').on('click', '.suggest-tour-destination-city', function () { 1329 dropdown = $(this).closest('.dropdown'); 1330 terms = { 1331 q: '', 1332 transport: $(this).closest('form, .searchbox').find('.suggest-tour-transport').val() || '' 1333 }; 1334 city_suggestion("/suggest/tour-destination-city", terms, dropdown); 1335}); 1336 1337$('body').on('click', '.suggest-pack-destination-city', function () { 1338 searchBar = $(this).closest('.search-bar'); 1339 dropdown = $(this).closest('.dropdown'); 1340 terms = { 1341 q: '' 1342 }; 1343 city_suggestion("/suggest/pack-destination-city", terms, dropdown); 1344}); 1345$('body').on('keyup', '.suggest-pack-destination-city', function () { 1346 clearTimeout(suggestionTimer); 1347 var self = this; 1348 suggestionTimer = setTimeout(function () { 1349 searchBar = $(self).closest('.search-bar'); 1350 dropdown = $(self).closest('.dropdown'); 1351 terms = { 1352 q: $(self).val() 1353 }; 1354 if (!$(self).hasClass('show')) { 1355 isTyping = true; 1356 let myDropdown = 1357 new bootstrap.Dropdown(dropdown, { 1358 autoClose: true, 1359 display: 'static' 1360 }); 1361 myDropdown.show(); 1362 isTyping = false; 1363 } 1364 city_suggestion("/suggest/pack-destination-city", terms, dropdown); 1365 }, 300); 1366}); 1367$('body').on('click', '.suggest-city-romania', function () { 1368 dropdown = $(this).closest('.dropdown'); 1369 terms = { 1370 q: '' 1371 }; 1372 city_suggestion("/suggest/city-romania", terms, dropdown); 1373}); 1374$('body').on('keyup', '.suggest-city-romania', function () { 1375 clearTimeout(suggestionTimer); 1376 var self = this; 1377 suggestionTimer = setTimeout(function () { 1378 dropdown = $(self).closest('.dropdown'); 1379 terms = { 1380 q: $(self).val() 1381 }; 1382 if (!$(self).hasClass('show')) { 1383 isTyping = true; 1384 let myDropdown = 1385 new bootstrap.Dropdown(dropdown, { 1386 autoClose: true, 1387 display: 'static' 1388 }); 1389 myDropdown.show(); 1390 isTyping = false; 1391 } 1392 city_suggestion("/suggest/city-romania", terms, dropdown); 1393 }, 300); 1394}); 1395 1396function calcTotalTravellers(travellersWrap) { 1397 adt = parseInt(travellersWrap.find('.travellers-adt').val()); 1398 kids = parseInt(travellersWrap.find('.travellers-kids').val()); 1399 rooms = parseInt(travellersWrap.find('.travellers-rooms').val()); 1400 if (kids > 0) { 1401 txt = adt + (adt == 1 ? ' Adult' : ' Adulti') + ', ' + kids + (kids == 1 ? ' Copil' : ' Copii'); 1402 } else { 1403 txt = adt + (adt == 1 ? ' Adult' : ' Adulti'); 1404 } 1405 1406 // ActualizeazÄ doar inputul din secÈiunea curentÄ 1407 var section = travellersWrap.closest('.travellers-section'); 1408 if (section.length) { 1409 section.find('.travellers').val(txt); 1410 section.find('.travellers-preview').html(txt); 1411 } else { 1412 // fallback - cautÄ Ã®n cel mai apropiat form/search-bar 1413 var parent = travellersWrap.closest('.search-bar, form'); 1414 if (parent.length) { 1415 parent.find('.travellers').val(txt); 1416 parent.find('.travellers-preview').html(txt); 1417 } else { 1418 travellersWrap.closest('.dropdown, .col-lg-12').find('.travellers').val(txt); 1419 } 1420 } 1421} 1422 1423// Sincronizare dropdown cÄlÄtori la deschidere 1424$('body').on('shown.bs.dropdown', '.travellers-section .dropdown', function () { 1425 var section = $(this).closest('.travellers-section'); 1426 var travellersInput = section.find('input.travellers'); 1427 var travellersWrap = $(this).find('.travellers-wrap'); 1428 var val = travellersInput.val() || ''; 1429 1430 // ParsÄm textul din input (ex: "1 Adult", "2 Adulti, 1 Copil") 1431 var adtMatch = val.match(/(\d+)\s*Adult/i); 1432 var kidsMatch = val.match(/(\d+)\s*Cop/i); 1433 // DacÄ inputul e gol, citim din hidden input existent 1434 var currentAdt = parseInt(travellersWrap.find('.travellers-adt').val()) || 1; 1435 var adt = adtMatch ? parseInt(adtMatch[1]) : currentAdt; 1436 var kids = kidsMatch ? parseInt(kidsMatch[1]) : 0; 1437
1438 // ActualizÄm hidden inputs Èi display-urile 1439 travellersWrap.find('.travellers-adt').val(adt); 1440 travellersWrap.find('.travellers-kids').val(kids); 1441 1442 // ActualizÄm numerele afiÈate 1443 var adtControl = travellersWrap.find('.travellers-adt').closest('.travellers-control-wrap'); 1444 adtControl.find('.number').html(adt); 1445 1446 var kidsControl = travellersWrap.find('.travellers-kids').closest('.travellers-control-wrap'); 1447 kidsControl.find('.number').html(kids); 1448 1449 // AfiÈÄm/ascundem secÈiunea copii 1450 if (kids > 0) { 1451 travellersWrap.find('.kids-section').show(); 1452 } else { 1453 travellersWrap.find('.kids-section').hide(); 1454 } 1455}); 1456 1457$('body').on('click', '.travellers-control .minus', function (e) { 1458 e.preventDefault(); 1459 var travellersWrap = $(this).parents('.travellers-wrap'); 1460 var controlWrap = $(this).parents('.travellers-control-wrap'); 1461 var hidden = controlWrap.find('.travellers-control-hidden'); 1462 var number = controlWrap.find('.number'); 1463 var val = parseInt(hidden.val()); 1464 1465 if (parseInt(hidden.attr('min')) < val && parseInt(hidden.attr('max')) >= val) { 1466 hidden.val(val - 1).trigger('change'); 1467 number.html(val - 1); 1468 } 1469 1470 calcTotalTravellers(travellersWrap); 1471 return false; 1472}); 1473$('body').on('click', '.travellers-control .plus', function (e) { 1474 e.preventDefault(); 1475 var travellersWrap = $(this).parents('.travellers-wrap'); 1476 var controlWrap = $(this).parents('.travellers-control-wrap'); 1477 var hidden = controlWrap.find('.travellers-control-hidden'); 1478 var number = controlWrap.find('.number'); 1479 var val = parseInt(hidden.val()); 1480 1481 if (parseInt(hidden.attr('min')) <= val && parseInt(hidden.attr('max')) > val) { 1482 hidden.val(val + 1).trigger('change'); 1483 number.html(val + 1); 1484 } 1485 1486 calcTotalTravellers(travellersWrap); 1487 return false; 1488}); 1489$('body').on('change', '.travellers-kids', function () { 1490 section = $(this).closest('.travellers-wrap'); 1491 if ($(this).val() > 0) { 1492 section.find('.kids-section').show(); 1493 details = section.find('.kids-details'); 1494 details.html(''); 1495 for (i = 0; i < $(this).val(); i++) { 1496 details.append(kidsAgeSelectHtml()); 1497 } 1498 } else { 1499 section.find('.kids-section').hide(); 1500 section.find('.kids-details').html(''); 1501 } 1502}); 1503 1504// For Mobile 1505 1506// ----------------------------------------------------------------------------- 1507// HOTEL DETAIL MOBILE OFFCANVAS (#hotelSearchMobile) - FORCE DATES INPUT SYNC 1508// This fixes the case where the embedded calendar updates selection UI but does 1509// not update the visible readonly input. 1510// ----------------------------------------------------------------------------- 1511(function () { 1512 const OFFCANVAS_SEL = '#hotelSearchMobile'; 1513 const INPUT_SEL = '#hotelSearchMobile .dates-mobile'; 1514 const SUMMARY_SEL = '#hotelSearchMobile #mobile-dates-summary'; 1515 const CAL_WRAP_SEL = '#hotelSearchMobile .calendar-container'; 1516 1517 function pad2(n) { return (n < 10 ? '0' : '') + n; } 1518 1519 function formatDate(d) { 1520 if (!(d instanceof Date) || isNaN(d.getTime())) return ''; 1521 const day = d.getDate(); // no leading 0 (matches your UI)
1522 const month = d.toLocaleString('en-US', { month: 'short' }); 1523 const year = d.getFullYear(); 1524 return `${day} ${month} ${year}`; 1525 } 1526 1527 function parseTs(ts) { 1528 if (!ts) return null; 1529 const s = String(ts).trim(); 1530 if (!s.length) return null; 1531 1532 let n = Number(s); 1533 if (!Number.isFinite(n)) return null; 1534 1535 // If seconds, convert to ms 1536 if (s.length === 10) n = n * 1000; 1537 1538 const d = new Date(n); 1539 if (isNaN(d.getTime())) return null; 1540 return d; 1541 } 1542 1543 function readRangeFromCalendarDom() { 1544 const cal = $(CAL_WRAP_SEL); 1545 if (!cal.length) return ''; 1546 1547 // Common Easepick-ish day markers (covers different class naming variants) 1548 const startEl = cal.find( 1549 '.day.start, .day.range-start, .day.is-start-date, .day.start-date, .day.selected.start' 1550 ).first(); 1551 1552 const endEl = cal.find( 1553 '.day.end, .day.range-end, .day.is-end-date, .day.end-date, .day.selected.end' 1554 ).first(); 1555 1556 // Many calendars store a timestamp on day nodes: 1557 // data-time="1707264000000" or data-time="1707264000" 1558 const startTs = startEl.attr('data-time') ?? startEl.data('time'); 1559 const endTs = endEl.attr('data-time') ?? endEl.data('time'); 1560 1561 const startDate = parseTs(startTs); 1562 const endDate = parseTs(endTs); 1563 1564 if (startDate && endDate) { 1565 return `${formatDate(startDate)} - ${formatDate(endDate)}`; 1566 } 1567 1568 // If only one is available (single date mode) 1569 if (startDate) return formatDate(startDate); 1570 if (endDate) return formatDate(endDate); 1571 1572 // Fallback: sometimes a hidden input inside the calendar holds the range. 1573 // Acceptam doar valori in format display ("DD MMM YYYY [- DD MMM YYYY]") 1574 // â adica trebuie sa contina si cifre si litere. Excludem explicit 1575 // formatul ISO "YYYY-MM-DD" (care e stocat in #rangepicker-* si ar 1576 // suprascrie valoarea vizibila formatata din PHP). 1577 const ISO_ONLY = /^\d{4}-\d{2}-\d{2}(\s*-\s*\d{4}-\d{2}-\d{2})?$/; 1578 const candidates = cal.find('input').map(function () { 1579 return String($(this).val() || '').trim(); 1580 }).get().filter(function (v) { 1581 if (!v.length) return false; 1582 if (ISO_ONLY.test(v)) return false; 1583 return /[0-9]/.test(v) && /[A-Za-z]/.test(v); 1584 }); 1585 1586 // Prefer strings that look like "12 Feb 2026 - 15 Feb 2026" 1587 const best = candidates.sort((a, b) => b.length - a.length) 1588 .find(v => v.includes('-')) || candidates[0] || ''; 1589 1590 return best; 1591 } 1592 1593 function setInputValue(v) { 1594 v = String(v || '').trim(); 1595 if (!v) return; 1596 1597 const $input = $(INPUT_SEL); 1598 if (!$input.length) return; 1599 1600 if ($input.val() !== v) { 1601 $input.val(v).trigger('input').trigger('change'); 1602 } 1603 1604 const $summary = $(SUMMARY_SEL); 1605 if ($summary.length && $summary.text().trim() !== v) { 1606 $summary.text(v); 1607 } 1608 } 1609 1610 function syncOnce() { 1611 // Only run if offcanvas exists (and ideally is visible) 1612 const oc = $(OFFCANVAS_SEL); 1613 if (!oc.length) return; 1614 1615 // Compute from calendar DOM (source of truth) 1616 const computed = readRangeFromCalendarDom(); 1617 if (computed) { 1618 setInputValue(computed); 1619 } 1620 } 1621 1622 let intervalId = null; 1623 1624 function startSyncLoop() { 1625 stopSyncLoop(); 1626 syncOnce(); 1627 intervalId = setInterval(function () { 1628 // Stop if offcanvas is gone 1629 if (!$(OFFCANVAS_SEL).length) return stopSyncLoop(); 1630 // If it is not visible, don't waste cycles 1631 if (!$(OFFCANVAS_SEL).hasClass('show')) return; 1632 syncOnce(); 1633 }, 200); 1634 } 1635 1636 function stopSyncLoop() { 1637 if (intervalId) { 1638 clearInterval(intervalId); 1639 intervalId = null; 1640 } 1641 } 1642 1643 // Bootstrap offcanvas lifecycle 1644 $(document).on('shown.bs.offcanvas', OFFCANVAS_SEL, function () { 1645 startSyncLoop(); 1646 }); 1647 1648 $(document).on('hidden.bs.offcanvas', OFFCANVAS_SEL, function () { 1649 stopSyncLoop(); 1650 }); 1651 1652 // Also attempt sync on any interaction with the calendar 1653 $(document).on('click touchend', `${CAL_WRAP_SEL} *`, function () { 1654 setTimeout(syncOnce, 0); 1655 }); 1656 1657 // If user focuses the input, refresh (useful when opening) 1658 $(document).on('click', INPUT_SEL, function () { 1659 setTimeout(syncOnce, 0); 1660 }); 1661})(); 1662 1663 1664// ----------------------------------------------------------------------------- 1665// EXISTING MOBILE SEARCH OFFCANVAS (#offcanvasMobile) LOGIC (unchanged mostly) 1666// ----------------------------------------------------------------------------- 1667 1668function normalizeTourMonthPreview(val) { 1669 const v = String(val || '').trim(); 1670 if (!v) return ''; 1671 1672 const noTime = v.replace(/\s+\d{2}:\d{2}:\d{2}\s*$/, ''); 1673 1674 // If itâs a full date like YYYY-MM-DD, show only YYYY-MM 1675 const m = noTime.match(/^(\d{4})-(\d{2})-(\d{2})$/); 1676 if (m) return `${m[1]}-${m[2]}`; 1677 1678 return noTime; 1679} 1680 1681// FuncÈii utilitare 1682function city_suggestion_mobile(url, terms, container) { 1683 $.ajax({ 1684 url: url, 1685 type: 'POST', 1686 async: true, 1687 cache: false,
1688 headers: { 'X-Update-Token': window._updateToken || '' }, 1689 data: terms, 1690 success: function (data) { 1691 container.find('.suggestions').html(data); 1692 }, 1693 beforeSend: function () { 1694 container.find('.suggestions').html( 1695 '<div class="text-center py-3"><i class="spinner-border spinner-border-sm"></i></div>' 1696 ); 1697 }, 1698 error: function () { 1699 container.find('.suggestions').html( 1700 '<div class="text-center py-3">Nu merg sugestiile.</div>'); 1701 notification("Nu merg sugestiile.", "danger"); 1702 } 1703 }); 1704} 1705 1706// DeterminÄm tipul de cÄutare curent 1707function determineSearchType(targetMenu) { 1708 if (targetMenu.includes('tour')) return 'tour'; 1709 if (targetMenu.includes('individual')) return 'individual'; 1710 if (targetMenu.includes('intern')) return 'intern'; 1711 if (targetMenu.includes('charter')) return 'charter'; 1712 return 'pack'; // Default 1713} 1714 1715// IniÈializÄm calendarul pentru tabul curent 1716function initializeCalendarForCurrentTab() { 1717 const activeTab = $('.search-type-content:not(.d-none)'); 1718 const calendarContainer = activeTab.find('.calendar-container'); 1719 1720 if (calendarContainer.length && calendarContainer.data('picker')) { 1721 const pickerId = calendarContainer.data('picker'); 1722 if (window[pickerId]) { 1723 window[pickerId].renderAll(); 1724 } 1725 } 1726} 1727 1728// IniÈializare selector de luni pentru circuite 1729// Inject CSS for custom mobile month picker (runs once) 1730(function () { 1731 if (document.getElementById('tour-mpicker-css')) return; 1732 var s = document.createElement('style'); 1733 s.id = 'tour-mpicker-css'; 1734 s.textContent = 1735 '.tour-mpicker-year{margin-bottom:16px}' + 1736 '.tour-mpicker-year-label{font-size:13px;font-weight:600;color:#6c757d;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}' + 1737 '.tour-mpicker-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}' + 1738 '.tour-mpicker-btn{padding:12px 4px;border:1px solid #dee2e6;border-radius:8px;background:#fff;font-size:14px;text-align:center;cursor:pointer;width:100%}' + 1739 '.tour-mpicker-btn.is-selected{background:var(--bs-primary,#0d6efd);color:#fff;border-color:var(--bs-primary,#0d6efd)}' + 1740 '.tour-mpicker-btn.is-disabled{opacity:.35;pointer-events:none}'; 1741 document.head.appendChild(s); 1742}()); 1743 1744function initializeTourMonthPickerMobile(destinationCode, presetRange) { 1745 const inputEl = document.getElementById('monthpicker-mobile'); 1746 if (!inputEl) return; 1747 1748 const containerEl = document.querySelector('#offcanvasMobile #tourCalendarMenu .monthpicker-container'); 1749 1750 // Show loading state immediately while AJAX runs 1751 if (containerEl) { 1752 containerEl.innerHTML = '<p class="text-muted text-center py-3 small">Se încarcÄ...</p>'; 1753 } 1754 1755 const renderPicker = (validRange) => { 1756 if (!containerEl) return; 1757 containerEl.innerHTML = ''; 1758 1759 const shortMonths = ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 1760 const now = new Date(); 1761 const currentVal = String($(inputEl).val() || '').trim(); 1762 1763 // Build set of available year-month keys from validRange date strings 1764 const validSet = new Set(); 1765 if (validRange && validRange.length > 0) { 1766 validRange.forEach(function (dateStr) { 1767 var d = new Date(dateStr); 1768 if (!isNaN(d.getTime())) { 1769 validSet.add(d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0')); 1770 } 1771 }); 1772 } 1773 1774 // Parse currently selected month from input value ("Apr 2026") 1775 let selYear = null, selMonthIdx = -1; 1776 if (currentVal) { 1777 const parts = currentVal.split(' '); 1778 if (parts.length === 2) { 1779 selMonthIdx = shortMonths.indexOf(parts[0]); 1780 selYear = parseInt(parts[1], 10); 1781 } 1782 } 1783 1784 // Render month grid for current year and next year
1785 const startYear = now.getFullYear(); 1786 for (let year = startYear; year <= startYear + 1; year++) { 1787 const firstMonth = (year === startYear) ? now.getMonth() : 0; 1788 const yearDiv = document.createElement('div'); 1789 yearDiv.className = 'tour-mpicker-year'; 1790 1791 const label = document.createElement('div'); 1792 label.className = 'tour-mpicker-year-label'; 1793 label.textContent = year; 1794 yearDiv.appendChild(label); 1795 1796 const grid = document.createElement('div'); 1797 grid.className = 'tour-mpicker-grid'; 1798 1799 for (let m = firstMonth; m < 12; m++) { 1800 const monthKey = year + '-' + String(m + 1).padStart(2, '0'); 1801 // null = AJAX error (allow all); [] = no tours (disable all); [...] = only those months 1802 const isAvailable = validRange === null || validSet.has(monthKey); 1803 const isSelected = year === selYear && m === selMonthIdx; 1804 const displayVal = shortMonths[m] + ' ' + year; 1805 1806 const btn = document.createElement('button'); 1807 btn.type = 'button'; 1808 btn.className = 'tour-mpicker-btn' + 1809 (isSelected ? ' is-selected' : '') + 1810 (!isAvailable ? ' is-disabled' : ''); 1811 btn.textContent = shortMonths[m]; 1812 1813 if (isAvailable) { 1814 btn.addEventListener('click', function () { 1815 $(inputEl).val(displayVal); 1816 1817 const $offcanvas = $('#offcanvasMobile'); 1818 $offcanvas.find('#tourMainMenu .calendar-preview').text(normalizeTourMonthPreview(displayVal)); 1819 1820 // Circuite: mai departe la Calatori (inainte: inapoi la meniul principal) 1821 goToNextTourSubmenu($offcanvas, '#tourCalendarMenu', '#tourMainMenu'); 1822 }); 1823 } 1824 1825 grid.appendChild(btn); 1826 } 1827 1828 yearDiv.appendChild(grid); 1829 containerEl.appendChild(yearDiv); 1830 } 1831 1832 // Add "Toate datele" button at the bottom 1833 const allDatesBtn = document.createElement('button'); 1834 allDatesBtn.type = 'button'; 1835 allDatesBtn.className = 'btn btn-outline-primary rounded-pill fw-semibold'; 1836 allDatesBtn.textContent = 'Toate datele'; 1837 allDatesBtn.style.cssText = 'margin: 16px 12px 12px 12px; width: calc(100% - 24px); padding: 12px 16px; font-size: 14px;'; 1838 allDatesBtn.addEventListener('click', function () { 1839 $(inputEl).val('Toate datele').trigger('change'); 1840 const $offcanvas = $('#offcanvasMobile'); 1841 $offcanvas.find('#tourMainMenu .calendar-preview').text('Toate datele'); 1842 goToNextTourSubmenu($offcanvas, '#tourCalendarMenu', '#tourMainMenu'); // Circuite 1843 }); 1844 containerEl.appendChild(allDatesBtn); 1845 }; 1846 1847 // Circuite: fara AJAX cand lunile vin gata calculate (searchbox-ul pe buget, data-tour-dates) sau 1848 // cand nu e aleasa nicio destinatie â atunci toate lunile, ca la cautarea fara destinatie. 1849 if (presetRange !== undefined) { 1850 renderPicker(Array.isArray(presetRange) ? presetRange : null); 1851 return; 1852 } 1853 if (!destinationCode) { 1854 renderPicker(null); 1855 return; 1856 } 1857 1858 $.ajax({ 1859 url: "/suggest/tour-dates", 1860 type: 'POST', 1861 async: true, 1862 cache: false, 1863 headers: { 'X-Update-Token': window._updateToken || '' }, 1864 data: { destination_code: destinationCode }, 1865 success: function (data) { 1866 let dates = null; 1867 try { dates = JSON.parse(data); } catch (e) { dates = null; } 1868 renderPicker(dates); 1869 }, 1870 error: function () { 1871 renderPicker(null); 1872 } 1873 }); 1874} 1875 1876// Circuite (mobil): âDate cÄlÄtorie" deschis direct din meniul principal, fara sa fi ales destinatia in 1877// sesiunea asta (ex. /circuite/turcia/, destinatie precompletata) â grila nu exista, construim una: cu 1878// lunile destinatiei curente, cu cele din data-tour-dates (buget) sau, fara destinatie, cu toate lunile. 1879function ensureTourMonthGridMobile() { 1880 const $menu = $('#offcanvasMobile #tourCalendarMenu').first(); 1881 if (!$menu.length || $menu.find('.tour-mpicker-btn').length) return; // deja construita 1882 const $tab = $menu.closest('.search-type-content'); 1883 const preset = $tab.data('tour-dates'); 1884 if (preset !== undefined) { 1885 initializeTourMonthPickerMobile(null, preset); 1886 return; 1887 } 1888 initializeTourMonthPickerMobile($.trim($tab.find('.destination-code').first().val() || '')); 1889} 1890// Mobile month picker for budget tour searchbox (#offcanvasMobileTour) 1891// Reads available dates from data attribute (no AJAX â no destination step) 1892function initializeBudgetTourMonthPickerMobile() { 1893 const inputEl = document.getElementById('monthpicker-mobile'); 1894 if (!inputEl) return; 1895 1896 const containerEl = document.querySelector('#offcanvasMobileTour #tourCalendarMenuResults .monthpicker-container'); 1897 if (!containerEl) return; 1898 1899 const rawDates = $('#offcanvasMobileTour #tour-content').data('tour-dates'); 1900 const validRange = Array.isArray(rawDates) ? rawDates : null; 1901 1902 containerEl.innerHTML = ''; 1903 1904 const shortMonths = ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 1905 const now = new Date(); 1906 const currentVal = String($(inputEl).val() || '').trim(); 1907 1908 const validSet = new Set(); 1909 if (validRange && validRange.length > 0) { 1910 validRange.forEach(function (dateStr) { 1911 var d = new Date(dateStr); 1912 if (!isNaN(d.getTime())) { 1913 validSet.add(d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0')); 1914 } 1915 }); 1916 } 1917 1918 let selYear = null, selMonthIdx = -1; 1919 if (currentVal) { 1920 const parts = currentVal.split(' '); 1921 if (parts.length === 2) { 1922 selMonthIdx = shortMonths.indexOf(parts[0]); 1923 selYear = parseInt(parts[1], 10); 1924 } 1925 } 1926
1927 const startYear = now.getFullYear(); 1928 for (let year = startYear; year <= startYear + 1; year++) { 1929 const firstMonth = (year === startYear) ? now.getMonth() : 0; 1930 const yearDiv = document.createElement('div'); 1931 yearDiv.className = 'tour-mpicker-year'; 1932 1933 const label = document.createElement('div'); 1934 label.className = 'tour-mpicker-year-label'; 1935 label.textContent = year; 1936 yearDiv.appendChild(label); 1937 1938 const grid = document.createElement('div'); 1939 grid.className = 'tour-mpicker-grid'; 1940 1941 for (let m = firstMonth; m < 12; m++) { 1942 const monthKey = year + '-' + String(m + 1).padStart(2, '0'); 1943 const isAvailable = validRange === null || validSet.has(monthKey); 1944 const isSelected = year === selYear && m === selMonthIdx; 1945 const displayVal = shortMonths[m] + ' ' + year; 1946 1947 const btn = document.createElement('button'); 1948 btn.type = 'button'; 1949 btn.className = 'tour-mpicker-btn' + 1950 (isSelected ? ' is-selected' : '') + 1951 (!isAvailable ? ' is-disabled' : ''); 1952 btn.textContent = shortMonths[m]; 1953 1954 if (isAvailable) { 1955 btn.addEventListener('click', function () { 1956 $(inputEl).val(displayVal); 1957 1958 const $offcanvas = $('#offcanvasMobileTour'); 1959 $offcanvas.find('#mainMenu .calendar-preview').text(normalizeTourMonthPreview(displayVal)); 1960 1961 // Circuite pe buget: campul urmator e Bugetul (inainte: inapoi la meniul principal) 1962 goToNextTourSubmenu($offcanvas, '#tourCalendarMenuResults', '#mainMenu'); 1963 }); 1964 } 1965 1966 grid.appendChild(btn); 1967 } 1968 1969 yearDiv.appendChild(grid); 1970 containerEl.appendChild(yearDiv); 1971 } 1972 1973 // Add "Toate datele" button at the bottom 1974 const allDatesBtn = document.createElement('button'); 1975 allDatesBtn.type = 'button'; 1976 allDatesBtn.className = 'btn btn-outline-primary rounded-pill fw-semibold'; 1977 allDatesBtn.textContent = 'Toate datele'; 1978 allDatesBtn.style.cssText = 'margin: 16px 12px 12px 12px; width: calc(100% - 24px); padding: 12px 16px; font-size: 14px;'; 1979 allDatesBtn.addEventListener('click', function () { 1980 $(inputEl).val('Toate datele').trigger('change'); 1981 const $offcanvas = $('#offcanvasMobileTour'); 1982 $offcanvas.find('#mainMenu .calendar-preview').text('Toate datele'); 1983 goToNextTourSubmenu($offcanvas, '#tourCalendarMenuResults', '#mainMenu'); // Circuite 1984 }); 1985 containerEl.appendChild(allDatesBtn); 1986} 1987 1988function syncTourTransportPreview($ctx) { 1989 const $radio = $ctx.find('#tourTransportMenu .transport-radio:checked'); 1990 const $hidden = $ctx.find('#tourTransportMenu .transport'); 1991 const val = $radio.length ? $radio.val() : ($hidden.length ? $hidden.val() : ''); 1992 const label = val ? val : 'Oricare'; 1993 1994 $ctx.find('#tourMainMenu .transport-preview').text(label); 1995} 1996 1997// When mobile offcanvas opens 1998$(document).on('shown.bs.offcanvas', '#offcanvasMobile', function () { 1999 syncTourTransportPreview($(this)); 2000}); 2001 2002// When user switches search type to "tour" in the selector 2003$(document).on('change', '.search-type-selector', function () { 2004 if ($(this).val() === 'tour') { 2005 syncTourTransportPreview($('#offcanvasMobile')); 2006 } 2007}); 2008 2009 2010// Actualizarea datelor calendarului 2011function updateCalendarDates(searchBar, departuresDates) { 2012 if (!departuresDates) return; 2013 2014 // BUGFIX: was using "dates" which is undefined here 2015 searchBar.attr('data-departure-dates', JSON.stringify(departuresDates)); 2016 searchBar.find('.departure-dates').val(JSON.stringify(departuresDates)); 2017 searchBar.find('.dates, .dates-mobile').val(''); 2018 searchBar.find('.start-at, .end-at').val(''); // DEV-577: data perechii vechi nu mai pleaca ascunsa 2019 searchBar.find('.calendar-preview').text('Alege datele'); 2020 2021 const activeTab = $('.search-type-content:not(.d-none)'); 2022 const calendarContainer = activeTab.find('.calendar-container'); 2023 2024 if (calendarContainer.length) { 2025 const pickerId = calendarContainer.data('picker'); 2026 if (window[pickerId]) {
2027 const keys = Object.keys(departuresDates); 2028 2029 if (keys && keys.length > 0) { 2030 const dateObj = new Date(keys[0]); 2031 const day = dateObj.getDate().toString().padStart(2, '0'); 2032 const month = dateObj.toLocaleString("en-US", { month: "short" }); 2033 const year = dateObj.getFullYear(); 2034 2035 const formattedDate = `${day} ${month} ${year}`; 2036 window[pickerId].gotoDate(formattedDate); 2037 } 2038 2039 window[pickerId].renderAll(); 2040 } 2041 } 2042} 2043 2044 2045// Navigarea între meniuri 2046$('body').on('click', '.menu-item', function () { 2047 targetMenu = '#' + $(this).data('target'); 2048 2049 $('#mainMenu, #tourMainMenu, #individualMainMenu, #internMainMenu, #charterMainMenu').addClass('hidden'); 2050 $('.submenu-container').removeClass('active'); 2051 $(targetMenu).addClass('active'); 2052 2053 const activeTab = $('.search-type-content:not(.d-none)'); 2054 const searchType = activeTab.attr('id').split('-')[0]; 2055 2056 if (targetMenu.includes(searchType + 'DestinationMenu')) { 2057 let mobile_container = $(targetMenu); 2058 let terms = { q: '' }; 2059 let searchType = determineSearchType(targetMenu); 2060 2061 if (destination_is_departure_mobile) { 2062 terms.departure_city_code = mobile_container.find('.departure-code').val(); 2063 } 2064 2065 let url = packSuggestUrl('destination-zone'); 2066 if (searchType === 'individual') { 2067 url = "/suggest/pack-destination-city"; 2068 } else if (searchType === 'tour') { 2069 url = "/suggest/tour-destination-city"; 2070 } else if (searchType === 'intern') { 2071 url = "/suggest/city-romania"; 2072 } else if (searchType === 'charter') { 2073 url = "/suggest/charter-destination-zone"; 2074 } 2075 2076 if (mobile_container.find('.suggestions').is(':empty')) { 2077 city_suggestion_mobile(url, terms, mobile_container); 2078 } 2079 } 2080 else if (targetMenu.includes(searchType + 'CityMenu')) { 2081 let container = $(targetMenu); 2082 let terms = { q: '' }; 2083 2084 if (!destination_is_departure_mobile) { 2085 terms.destination_code = $('.search-form:visible').find('.destination-code').val(); 2086 } 2087 2088 if (container.find('.suggestions').is(':empty')) { 2089 city_suggestion_mobile(packSuggestUrl('departure-city'), terms, container); 2090 } 2091 } 2092 else if (targetMenu === '#tourCalendarMenu' || targetMenu === '#tourCalendarMenuResults') { 2093 // Circuite: intrat direct pe âDate cÄlÄtorie", grila de luni nu se construia â ramura 2094 // `includes('calendarMenu')` de mai jos e case-sensitive si nu prinde âtourCalendarMenu". Tratam 2095 // explicit doar circuitele; Cazare / Romania / Bilete raman cum erau. 2096 if (targetMenu === '#tourCalendarMenuResults') initializeBudgetTourMonthPickerMobile(); 2097 else ensureTourMonthGridMobile(); 2098 } 2099 else if (targetMenu.includes('transportMenu')) { 2100 // nothing 2101 } 2102 else if (targetMenu.includes('calendarMenu')) { 2103 if (targetMenu === '#tourCalendarMenuResults') { 2104 initializeBudgetTourMonthPickerMobile(); 2105 } else { 2106 initializeCalendarForCurrentTab(); 2107 } 2108 } 2109}); 2110 2111// Butonul înapoi 2112$('body').on('click', '.back-btn', function () { 2113 // Budget tour offcanvas has #mainMenu (not #tourMainMenu) as its main menu 2114 if ($(this).closest('#offcanvasMobileTour').length > 0) { 2115 $('#offcanvasMobileTour .submenu-container').removeClass('active'); 2116 $('#offcanvasMobileTour #mainMenu').removeClass('hidden'); 2117 return; 2118 } 2119 2120 const activeTab = $('.search-type-content:not(.d-none)'); 2121 if (activeTab.attr('id') === 'pack-content') { 2122 $('#mainMenu').removeClass('hidden'); 2123 } else if (activeTab.attr('id') === 'tour-content') { 2124 $('#tourMainMenu').removeClass('hidden'); 2125 } else if (activeTab.attr('id') === 'individual-content') { 2126 $('#individualMainMenu').removeClass('hidden'); 2127 } else if (activeTab.attr('id') === 'intern-content') { 2128 $('#internMainMenu').removeClass('hidden'); 2129 } else if (activeTab.attr('id') === 'charter-content') { 2130 $('#charterMainMenu').removeClass('hidden'); 2131 } 2132 2133 $('.submenu-container').removeClass('active'); 2134}); 2135 2136// Selectorul de tip cÄutare 2137$('body').on('change', '.search-type-selector', function () { 2138 const selectedType = $(this).val(); 2139 $('.search-type-content').addClass('d-none'); 2140 $(`#${selectedType}-content`).removeClass('d-none'); 2141 2142 $('.submenu-container').removeClass('active'); 2143 $('#mainMenu, #tourMainMenu, #individualMainMenu, #internMainMenu, #charterMainMenu').removeClass('hidden'); 2144}); 2145 2146// Gestionarea cÄutÄrii destinaÈie 2147$('body').on('keyup', '.suggest-pack-destination-zone-mobile, .suggest-tour-destination-city-mobile, .suggest-pack-destination-city-mobile, .suggest-city-romania-mobile, .suggest-charter-destination-zone-mobile', function () { 2148 clearTimeout(suggestionTimer); 2149 var self = this; 2150 suggestionTimer = setTimeout(function () { 2151 let container; 2152 let url; 2153 let terms = { q: $(self).val() }; 2154 2155 if ($(self).hasClass('suggest-charter-destination-zone-mobile')) { 2156 container = $('#charterDestinationMenu');
2157 url = "/suggest/charter-destination-zone"; 2158 if (destination_is_departure_mobile) { 2159 terms.departure_city_code = $('#charterCityMenu').find('.departure-code').val(); 2160 } 2161 } else if ($(self).hasClass('suggest-pack-destination-zone-mobile')) { 2162 container = $('#packDestinationMenu'); 2163 url = packSuggestUrl('destination-zone'); 2164 if (destination_is_departure_mobile) { 2165 terms.departure_city_code = $('#packCityMenu').find('.departure-code').val(); 2166 } 2167 } else if ($(self).hasClass('suggest-tour-destination-city-mobile')) { 2168 container = $('#tourDestinationMenu'); 2169 url = "/suggest/tour-destination-city"; 2170 let transport = $('#tourTransportMenu').find('.transport').val(); 2171 if (transport) { 2172 terms.transport = transport; 2173 } 2174 } else if ($(self).hasClass('suggest-pack-destination-city-mobile')) { 2175 container = $('#individualDestinationMenu'); 2176 url = "/suggest/pack-destination-city"; 2177 } else if ($(self).hasClass('suggest-city-romania-mobile')) { 2178 container = $('#internDestinationMenu'); 2179 url = "/suggest/city-romania"; 2180 } 2181 2182 if (container && url) { 2183 city_suggestion_mobile(url, terms, container); 2184 } 2185 }, 300); 2186}); 2187 2188// Gestionarea cÄutÄrii oraÈ plecare 2189$('body').on('keyup', '.suggest-pack-departure-city-mobile', function () { 2190 clearTimeout(suggestionTimer); 2191 var self = this; 2192 suggestionTimer = setTimeout(function () { 2193 let container = $(self).closest('.submenu-container'); 2194 let terms = { q: $(self).val() }; 2195 2196 if (!destination_is_departure_mobile) { 2197 terms.destination_code = $('.search-form:visible').find('.destination-code').val(); 2198 } 2199 2200 city_suggestion_mobile(packSuggestUrl('departure-city'), terms, container); 2201 }, 300); 2202}); 2203 2204// SelecÈia destinaÈiei 2205$('body').on('click', '#offcanvasMobile .suggest-destination', function () { 2206 const section = $(this).closest('.submenu-container'); 2207 const searchBar = $(this).closest('#offcanvasMobile'); 2208 const activeTab = $('.search-type-content:not(.d-none)'); 2209 const searchType = activeTab.attr('id').split('-')[0]; 2210 2211 // ActualizÄm câmpurile cu destinaÈia selectatÄ 2212 section.find('.destination').val($(this).data('destination')); 2213 section.find('.destination-code').val($(this).data('destination-code')); 2214 2215 // ActualizÄm preview-ul 2216 searchBar.find('.destination-preview').html($(this).data('destination') + ', ' + $(this).data('destination-country')); 2217 2218 // AcÈiuni specifice în funcÈie de tip cÄutare 2219 if (destination_is_departure_mobile) { 2220 // DacÄ destinaÈia este Èi plecare, actualizÄm datele calendarului 2221 updateCalendarDates(activeTab, $(this).data('departure-dates')); 2222 } 2223 else if (section.find('.destination').hasClass('suggest-tour-destination-city-mobile')) { 2224 // Pentru circuite: deschidem selectorul de luni DOAR in meniul "Date calatorie" 2225 const destCode = $(this).data('destination-code'); 2226 2227 // Inchidem toate submeniurile si deschidem tourCalendarMenu 2228 searchBar.find('.submenu-container').removeClass('active'); 2229 searchBar.find('#tourCalendarMenu').addClass('active'); 2230 2231 // IMPORTANT: init dupa ce meniul devine vizibil (previne "sliver"/pozitionare gresita) 2232 setTimeout(function () { 2233 initializeTourMonthPickerMobile(destCode); 2234 }, 0); 2235 2236 return; // nu mai revenim la meniul principal in acest caz 2237 } 2238 else if (section.find('.destination').hasClass('suggest-city-romania-mobile')) { 2239 // Pentru turism intern 2240 activeTab.find('#internCalendarMenu').removeClass('active'); 2241 } 2242 else { 2243 // DEV-577 â alta destinatie = alta pereche: lista de plecari si data erau ale destinatiei vechi 2244 // (desktop face la fel pe .search-packs). Orasul se re-alege din lista reincarcata mai jos. 2245 if (isPackSearchForm(section.closest('form'))) resetPackPair(activeTab); 2246 2247 // Pentru celelalte tipuri, actualizÄm sugestiile pentru oraÈul de plecare 2248 const cityContainer = searchBar.find(`#${searchType}CityMenu, #packCityMenu`).first(); 2249 const terms = { 2250 q: '', 2251 destination_code: $(this).data('destination-code') 2252 }; 2253 2254 if (cityContainer.length) { 2255 city_suggestion_mobile(packSuggestUrl('departure-city'), terms, cityContainer); 2256 } 2257 2258 // Auto-advance: pack â deschide CityMenu 2259 if (searchType === 'pack' && cityContainer.length) { 2260 searchBar.find('.submenu-container').removeClass('active'); 2261 cityContainer.addClass('active'); 2262 return; 2263 } 2264 } 2265 2266 // Revenim la meniul principal 2267 searchBar.find('#mainMenu, #tourMainMenu, #individualMainMenu, #internMainMenu, #charterMainMenu').removeClass('hidden'); 2268 searchBar.find('.submenu-container').removeClass('active'); 2269}); 2270// SelecÈia oraÈului de plecare 2271$('body').on('click', '#offcanvasMobile .suggest-departure', function () { 2272 const section = $(this).closest('.submenu-container'); 2273 const searchBar = $(this).closest('#offcanvasMobile'); 2274 const activeTab = $('.search-type-content:not(.d-none)'); 2275 const searchType = activeTab.attr('id').split('-')[0]; 2276 2277 // ActualizÄm câmpurile cu oraÈul selectat 2278 section.find('.departure').val($(this).data('departure') + ', ' + $(this).data('departure-country')); 2279 section.find('.departure-code').val($(this).data('departure-code')); 2280 2281 // ActualizÄm preview-ul 2282 searchBar.find('.departure-preview').html($(this).data('departure') + ', ' + $(this).data('departure-country')); 2283 2284 // AcÈiuni specifice în funcÈie de caz 2285 if (destination_is_departure_mobile == false) {
2286 // DacÄ plecarea nu este Èi destinaÈie, actualizÄm datele calendarului 2287 updateCalendarDates(activeTab, $(this).data('departure-dates')); 2288 } else { 2289 // DEV-577 â (ordine inversata) alt oras = alta pereche: lista de plecari si data le scrie abia 2290 // destinatia aleasa dupa el, deci nu pastram nimic din perechea veche. 2291 if (isPackSearchForm(section.closest('form'))) resetPackPair(activeTab); 2292 2293 // DacÄ plecarea este Èi destinaÈie, actualizÄm sugestiile pentru destinaÈie. 2294 // DEV-577: la Sejururi / 7+7 lista e #packDestinationMenu â selectorul vechi o rata, deci 2295 // destinatiile ramaneau fara plecarile orasului si alegerea lor nu mai scria lista de plecari. 2296 const destinationContainer = searchType === 'pack' 2297 ? activeTab.find('#packDestinationMenu') 2298 : searchBar.find('#destinationMenu, #charterDestinationMenu').first(); 2299 const terms = { 2300 q: '', 2301 departure_city_code: $(this).data('departure-code') 2302 }; 2303 2304 if (destinationContainer.length) { 2305 city_suggestion_mobile(packSuggestUrl('destination-zone'), terms, destinationContainer); 2306 } 2307 } 2308 2309 // Auto-advance: pack â deschide calendarMenu 2310 if (searchType === 'pack') { 2311 const calendarContainer = searchBar.find('#calendarMenu').first(); 2312 if (calendarContainer.length) { 2313 searchBar.find('.submenu-container').removeClass('active'); 2314 calendarContainer.addClass('active'); 2315 return; 2316 } 2317 } 2318 2319 // Revenim la meniul principal 2320 searchBar.find('#mainMenu, #charterMainMenu').removeClass('hidden'); 2321 searchBar.find('.submenu-container').removeClass('active'); 2322}); 2323 2324// Butonul switch pentru inversarea destinaÈie/plecare 2325$('body').on('click', '.btn-switch-mobile', function (e) { 2326 e.preventDefault(); 2327 2328 // DeterminÄm tipul de cÄutare curent 2329 const activeTab = $('.search-type-content:not(.d-none)'); 2330 const searchType = activeTab.attr('id').split('-')[0]; 2331 2332 const destinationSection = activeTab.find('[data-target="' + searchType + 'DestinationMenu"]'); 2333 const departureSection = activeTab.find('[data-target="' + searchType + 'CityMenu"]'); 2334 2335 if (destinationSection.length && departureSection.length) { 2336 // SchimbÄm poziÈiile între cele douÄ div-uri 2337 const destinationClone = destinationSection.clone(true); 2338 const departureClone = departureSection.clone(true); 2339 2340 // Ãnlocuim destinaÈia cu plecarea Èi invers 2341 destinationSection.replaceWith(departureClone); 2342 departureSection.replaceWith(destinationClone); 2343 2344 // InversÄm flag-ul 2345 destination_is_departure_mobile = !destination_is_departure_mobile; 2346 2347 // ActualizÄm sugestiile 2348 const destinationContainer = activeTab.find('#' + searchType + 'DestinationMenu'); 2349 const cityContainer = activeTab.find('#' + searchType + 'CityMenu'); 2350 2351 if (destinationContainer.length) { 2352 city_suggestion_mobile(packSuggestUrl('destination-zone'), "", destinationContainer); 2353 } 2354 2355 if (cityContainer.length) { 2356 city_suggestion_mobile(packSuggestUrl('departure-city'), "", cityContainer); 2357 } 2358 2359 // ActualizÄm preview-urile 2360 const destPreviewText = activeTab.find('.departure-preview').text(); 2361 const depPreviewText = activeTab.find('.destination-preview').text(); 2362 2363 activeTab.find('.destination-preview').text(destPreviewText); 2364 activeTab.find('.departure-preview').text(depPreviewText); 2365 } 2366}); 2367 2368// Navigare între ÈÄri/zone 2369$('body').on('click', '#offcanvasMobile .suggest-destination-country', function (e) { 2370 e.preventDefault(); 2371 const dropdown = $(this).closest('.suggestions'); 2372 2373 dropdown.find('.suggest-destination-country').removeClass('d-flex').addClass('d-none'); 2374 dropdown.find('.suggest-destination-popular').removeClass('d-flex').addClass('d-none'); 2375 2376 dropdown.find('.suggest-destination-' + $(this).data('country')) 2377 .removeClass('d-none').addClass('d-flex'); 2378 2379 dropdown.find('.suggest-destination-popular.suggest-destination-' + $(this).data('country')) 2380 .removeClass('d-flex').addClass('d-block'); 2381 2382 dropdown.find('.btn-back').removeClass('mt-3') 2383 .addClass('mt-3').removeClass('d-none').addClass('d-flex'); 2384 syncDestinationPopulars(dropdown); // DEV-574 â coloana de populare (stivuita sub lista pe mobil) 2385}); 2386$('body').on('click', '#offcanvasMobile .suggest-destination-zone', function (e) { 2387 e.preventDefault(); 2388 const dropdown = $(this).closest('.suggestions'); 2389 2390 dropdown.find('.suggest-destination-country').removeClass('d-flex').addClass('d-none'); 2391 dropdown.find('.suggest-destination-zone').removeClass('d-flex').addClass('d-none'); 2392 dropdown.find('.suggest-destination-popular').removeClass('d-flex').addClass('d-none'); 2393 2394 dropdown.find('.suggest-destination-' + $(this).data('zone')) 2395 .removeClass('d-none').addClass('d-flex'); 2396 2397 dropdown.find('.suggest-destination-popular.suggest-destination-' + $(this).data('zone')) 2398 .removeClass('d-flex').addClass('d-block'); 2399 2400 dropdown.find('.btn-back') 2401 .removeClass('d-none').addClass('d-flex mt-3'); 2402 syncDestinationPopulars(dropdown); // DEV-574 2403}); 2404// Butonul înapoi în sugestii 2405$('body').on('click', '#offcanvasMobile .btn-back', function (e) { 2406 e.preventDefault(); 2407 const dropdown = $(this).closest('.suggestions'); 2408 2409 dropdown.find('.suggest-destination-country').removeClass('d-none').addClass('d-flex'); 2410 dropdown.find('.col-md-4 .suggest-destination').removeClass('d-flex').addClass('d-none'); 2411 // DEV-574 â si zonele (Cazare): dupa tara -> Inapoi ramaneau amestecate cu lista de tari 2412 dropdown.find('.col-md-4 .suggest-destination-zone').removeClass('d-flex').addClass('d-none'); 2413 2414 dropdown.find('.suggest-destination-popular') 2415 .removeClass('d-block').removeClass('d-flex').removeClass('d-none'); 2416 2417 dropdown.find('.btn-back').removeClass('d-flex').addClass('d-none'); 2418 syncDestinationPopulars(dropdown); // DEV-574 2419}); 2420 2421// Gestionarea cÄlÄtorilor 2422$('body').on('change', '.travellers-adults, .travellers-kids', function () { 2423 const section = $(this).closest('.travellers-section'); 2424 const searchBar = $(this).closest('#offcanvasMobile'); 2425
2426 // Compunem textul pentru cÄlÄtori 2427 const adults = section.find('.travellers-adults').val() || 0; 2428 const kids = section.find('.travellers-kids').val() || 0; 2429 2430 let travellersText = ''; 2431 if (adults > 0) { 2432 travellersText += adults + (adults == 1 ? ' Adult' : ' Adulti'); 2433 } 2434 2435 if (kids > 0) { 2436 travellersText += (travellersText ? ', ' : '') + kids + (kids == 1 ? ' Copil' : ' Copii'); 2437 } 2438 2439 if (!travellersText) { 2440 travellersText = 'AdaugÄ persoane'; 2441 } 2442 2443 // ActualizÄm input Èi preview 2444 section.find('.travellers').val(travellersText); 2445 searchBar.find('.travellers-preview').text(travellersText); 2446 2447 // GestionÄm secÈiunea pentru vârstele copiilor 2448 if (kids > 0) { 2449 section.find('.kids-section').show(); 2450 const details = section.find('.kids-details'); 2451 details.html(''); 2452 2453 for (let i = 0; i < kids; i++) { 2454 details.append(kidsAgeSelectHtml()); 2455 } 2456 } else { 2457 section.find('.kids-section').hide(); 2458 section.find('.kids-details').html(''); 2459 } 2460}); 2461 2462// Gestionarea selecÈiei nopÈilor 2463$('body').on('change', '.nights', function () { 2464 const searchBar = $(this).closest('#offcanvasMobile'); 2465 const value = $(this).val(); 2466 const text = value + (value == 1 ? ' noapte' : ' nopÈi'); 2467 2468 searchBar.find('.nights-preview').text(text); 2469}); 2470 2471// Desktop: noptile sunt un <select> la capatul barei (Sejururi / 7+7 / Bilete) â alegerea lor e ultima 2472// selectie si trimite cautarea. Pe barele pe buget, unde dupa nopti urmeaza bugetul, nu. 2473$('body').on('change', '.search-bar select.nights', function () { 2474 if (!$(this).val()) return; // âAlege" nu e o alegere 2475 if (searchbarIsLastStep(this)) submitSearchbar(this); 2476}); 2477 2478// Gestionarea selecÈiei transportului 2479$('body').on('change', '.transport-radio', function () { 2480 const searchBar = $(this).closest('#offcanvasMobile'); 2481 const value = $(this).val(); 2482 2483 // ActualizÄm hidden input Èi preview 2484 searchBar.find('#tourTransportMenu .transport').val(value); 2485 searchBar.find('.transport-preview').text(value); 2486 2487 // Stil radio labels 2488 searchBar.find('.transport-radio-label').removeClass('border-primary bg-primary-subtle'); 2489 $(this).closest('.transport-radio-label').addClass('border-primary bg-primary-subtle'); 2490 2491 // ActualizÄm sugestiile pentru destinaÈii în funcÈie de transport 2492 const container = searchBar.find('#tourDestinationMenu'); 2493 const terms = { 2494 transport: value, 2495 q: '' 2496 }; 2497 city_suggestion_mobile("/suggest/tour-destination-city", terms, container); 2498 2499 // Auto-advance: transport â destinaÈie 2500 setTimeout(function () { 2501 searchBar.find('.submenu-container').removeClass('active'); 2502 searchBar.find('#tourMainMenu').addClass('hidden'); 2503 searchBar.find('#tourDestinationMenu').addClass('active'); 2504 }, 200); 2505}); 2506 2507// IniÈializare date cÄlÄtorie când sunt selectate 2508$('body').on('change', '.dates', function () { 2509 const searchBar = $(this).closest('#offcanvasMobile'); 2510 const value = $(this).val(); 2511 2512 if (value) { 2513 searchBar.find('.calendar-preview').text(value); 2514 2515 // Auto-advance: pack calendar â travelersMenu 2516 const activeTab = searchBar.find('.search-type-content:not(.d-none)'); 2517 if (activeTab.attr('id') === 'pack-content') { 2518 const travelersContainer = searchBar.find('#travelersMenu').first(); 2519 if (travelersContainer.length) { 2520 searchBar.find('.submenu-container').removeClass('active'); 2521 travelersContainer.addClass('active'); 2522 } 2523 } 2524 } 2525}); 2526 2527// Auto-advance: travelers â nightsMenu (dacÄ are opÈiuni multiple) sau mainMenu 2528$('body').on('click', '.travellers-continue-btn', function () { 2529 const searchBar = $(this).closest('#offcanvasMobile, .mobileOffcanvas'); 2530 const activeContent = searchBar.find('.search-type-content:not(.d-none)'); 2531 const scope = activeContent.length ? activeContent : searchBar; 2532 const nightsRadios = scope.find('.nights-radio-group .nights-radio-label'); 2533 2534 // ActualizeazÄ textul cÄlÄtorilor înainte de a naviga 2535 var travellersSection = $(this).closest('.travellers-section'); 2536 var travellersWrap = travellersSection.find('.travellers-wrap'); 2537 if (travellersWrap.length) { 2538 calcTotalTravellers(travellersWrap); 2539 // ActualizeazÄ preview-ul din main menu 2540 var txt = travellersSection.find('input.travellers').val(); 2541 if (txt) { 2542 scope.find('.travellers-preview').html(txt); 2543 searchBar.find('.travellers-preview').html(txt); 2544 } 2545 } 2546 2547 // Bara de cautare: pasul urmator dupa ordinea meniului (noptile, daca au > 1 optiune). Pe ultimul pas 2548 // butonul scrie âCautÄ": blocam ca âAplicÄ" de pe desktop daca lipsesc varstele, altfel trimitem. 2549 const isSearchbar = searchbarForm(this).length > 0; 2550 const $nextStep = isSearchbar ? searchbarNextStepMobile(this) : $(); 2551 if (isSearchbar && !$nextStep.length && travellersAgesBlocked(travellersSection)) return; 2552 2553 // DezactiveazÄ submenus 2554 searchBar.find('.submenu-container').removeClass('active'); 2555 2556 if ($nextStep.length) { 2557 $nextStep.addClass('active'); 2558 } else if (!isSearchbar && nightsRadios.length > 1) { 2559 // Mai multe opÈiuni de nopÈi - deschide nightsMenu din tab-ul activ 2560 scope.find('[id$="NightsMenu"], #nightsMenu').first().addClass('active'); 2561 } else { 2562 // Revine la mainMenu-ul din tab-ul activ 2563 scope.find('[id$="MainMenu"], #mainMenu').first().removeClass('hidden'); 2564 if (isSearchbar) submitSearchbar(this); 2565 } 2566}); 2567 2568// SelecÈie nopÈi radio - actualizeazÄ hidden input, preview, Èi revine la mainMenu 2569$('body').on('change', '.nights-radio', function () { 2570 const searchBar = $(this).closest('#offcanvasMobile'); 2571 const val = $(this).val(); 2572 2573 // ActualizÄm hidden input Èi preview 2574 searchBar.find('input.nights').val(val); 2575 searchBar.find('.nights-preview').text(val + ' nopÈi'); 2576 2577 // ActualizÄm stil 2578 searchBar.find('.nights-radio-label').removeClass('border-primary bg-primary-subtle'); 2579 $(this).closest('.nights-radio-label').addClass('border-primary bg-primary-subtle'); 2580 2581 // Revenim la meniul principal al tabului activ (DEV-539: pe tab-ul charter #mainMenu 2582 // e in pack-content ascuns, deci utilizatorul ramanea pe un ecran gol). 2583 // Noptile sunt ultimul pas al barei: alegerea lor trimite si cautarea. 2584 const self = this; 2585 const isLast = searchbarIsLastStep(this); 2586 setTimeout(function () { 2587 const activeContent = searchBar.find('.search-type-content:not(.d-none)'); 2588 const scope = activeContent.length ? activeContent : searchBar; 2589 searchBar.find('.submenu-container').removeClass('active'); 2590 scope.find('[id$="MainMenu"], #mainMenu').first().removeClass('hidden'); 2591 if (isLast) submitSearchbar(self); 2592 }, 200); 2593}); 2594 2595// DEV-539: CTA âContinuÄ" in submeniul de nopti (mobil). Radio-ul deja bifat nu emite 2596// `change`, deci fara buton utilizatorul ramanea blocat in âAlege noptile". 2597$('body').on('click', '.nights-continue-btn', function () { 2598 const searchBar = $(this).closest('#offcanvasMobile, .mobileOffcanvas'); 2599 const activeContent = searchBar.find('.search-type-content:not(.d-none)'); 2600 const scope = activeContent.length ? activeContent : searchBar; 2601 const checked = $(this).closest('.submenu-container').find('.nights-radio:checked'); 2602 2603 if (checked.length) { 2604 const val = checked.val(); 2605 searchBar.find('input.nights').val(val); 2606 searchBar.find('.nights-preview').text(val + ' nopÈi'); 2607 } 2608 2609 searchBar.find('.submenu-container').removeClass('active'); 2610 scope.find('[id$="MainMenu"], #mainMenu').first().removeClass('hidden'); 2611 if (searchbarIsLastStep(this)) submitSearchbar(this); // ultimul pas: butonul scrie âCautÄ" 2612}); 2613 2614// Searchbox-urile pe buget (mobil): âCautÄ" din submeniul de buget, cand bugetul e ultimul pas (Circuite / 2615// Cazare pe buget). Daca mai urmeaza un pas, ducem acolo; altfel meniul principal + cautarea. 2616$('body').on('click', '.budget-continue-btn', function () { 2617 const $offcanvas = $(this).closest('.offcanvas'); 2618 const $tab = $(this).closest('.search-type-content'); 2619 const $nextStep = searchbarNextStepMobile(this); 2620 $offcanvas.find('.submenu-container').removeClass('active'); 2621 if ($nextStep.length) { 2622 $nextStep.addClass('active'); 2623 return; 2624 } 2625 ($tab.length ? $tab : $offcanvas).find('[id$="MainMenu"], #mainMenu').first().removeClass('hidden'); 2626 submitSearchbar(this); 2627}); 2628 2629// IniÈializare la deschiderea offcanvas-ului 2630$('#offcanvasMobile').on('shown.bs.offcanvas', function () { 2631 // Get the first option value from the search-type-selector 2632 const firstSearchTypeValue = $('.search-type-selector option:first').val() || 'pack'; 2633 2634 // Set the selector to the first value and trigger change 2635 $('.search-type-selector').val(firstSearchTypeValue).trigger('change'); 2636 2637 // ResetÄm starea 2638 destination_is_departure_mobile = false;
2639 2640 // Ascundem toate submeniurile 2641 $('.submenu-container').removeClass('active'); 2642 2643 // Show the appropriate main menu based on the selected search type 2644 if (firstSearchTypeValue === 'pack') { 2645 $('#mainMenu').removeClass('hidden'); 2646 } else if (firstSearchTypeValue === 'tour') { 2647 $('#tourMainMenu').removeClass('hidden'); 2648 } else if (firstSearchTypeValue === 'individual') { 2649 $('#individualMainMenu').removeClass('hidden'); 2650 } else if (firstSearchTypeValue === 'intern') { 2651 $('#internMainMenu').removeClass('hidden'); 2652 } else if (firstSearchTypeValue === 'charter') { 2653 $('#charterMainMenu').removeClass('hidden'); 2654 } 2655 2656 // IniÈializÄm calendarul dacÄ este necesar 2657 const calendarContainers = $(this).find('.calendar-container'); 2658 if (calendarContainers.length) { 2659 calendarContainers.each(function () { 2660 const pickerId = $(this).data('picker'); 2661 if (window[pickerId]) { 2662 window[pickerId].renderAll(); 2663 } 2664 }); 2665 } 2666}); 2667 2668// Charter desktop keyup handlers 2669$('body').on('keyup', '.suggest-charter-destination-zone', function () { 2670 clearTimeout(suggestionTimer); 2671 var self = this; 2672 suggestionTimer = setTimeout(function () { 2673 searchBar = $(self).closest('.search-bar'); 2674 dropdown = $(self).closest('.dropdown'); 2675 terms = { q: $(self).val() }; 2676 if (destination_is_departure) { 2677 terms.departure_city_code = searchBar.find('.departure-code').val(); 2678 } 2679 if (!$(self).hasClass('show')) { 2680 isTyping = true; 2681 let myDropdown = new bootstrap.Dropdown(dropdown, { autoClose: true, display: 'static' }); 2682 myDropdown.show(); 2683 isTyping = false; 2684 } 2685 city_suggestion("/suggest/charter-destination-zone", terms, dropdown); 2686 }, 300); 2687}); 2688 2689$('body').on('keyup', '.suggest-charter-departure-city', function () { 2690 clearTimeout(suggestionTimer); 2691 var self = this; 2692 suggestionTimer = setTimeout(function () { 2693 dropdown = $(self).closest('.dropdown'); 2694 searchBar = $(self).closest('.search-bar'); 2695 terms = { q: $(self).val() }; 2696 if (!destination_is_departure) { 2697 terms.destination_code = searchBar.find('.destination-code').val(); 2698 } 2699 if (!$(self).hasClass('show')) { 2700 isTyping = true; 2701 let myDropdown = new bootstrap.Dropdown(dropdown, { autoClose: true, display: 'static' }); 2702 myDropdown.show(); 2703 isTyping = false; 2704 } 2705 city_suggestion("/suggest/charter-departure-city", terms, dropdown); 2706 }, 300); 2707}); 2708 2709// ========================================================================= 2710// 7+7 (Sejur + Circuit) â validare + switching intre moduri pentru tabul pack 2711// ========================================================================= 2712 2713// Validator pentru orice forma care posteaza catre /rezultate-cautare-pachete-7plus7 2714// (se aplica si la desktop-ul cu tab-ul 7+7, si la mobil cand selectorul e pe "7+7"). 2715$(document).on('submit', 'form', function (e) { 2716 var $form = $(this); 2717 var urlVal = $form.find('input[name="url"]').val(); 2718 if (urlVal !== '/rezultate-cautare-pachete-7plus7') return; 2719 2720 var nights = parseInt($form.find('[name="nights"]').val(), 10); 2721 if (isNaN(nights) || nights < 14) { 2722 e.preventDefault(); 2723 alert('Pentru pachetele 7+7 sunt necesare minim 14 nopÈi.'); 2724 return false; 2725 } 2726 var ddRaw = $form.find('.departure-dates').first().val(); 2727 if (ddRaw) { 2728 try { 2729 var dd = JSON.parse(ddRaw); 2730 var hasMin14 = Object.values(dd || {}).some(function (entry) { 2731 var list = Array.isArray(entry) ? entry : [entry]; 2732 return list.some(function (n) { return parseInt(n, 10) >= 14; }); 2733 }); 2734 if (!hasMin14) { 2735 e.preventDefault(); 2736 alert('DestinaÈia Èi oraÈul selectat nu au opÈiuni de minim 14 nopÈi. Alege altÄ combinaÈie.'); 2737 return false; 2738 } 2739 } catch (err) { /* ignore */ } 2740 } 2741}); 2742
2743// Desktop: la switch pe tab-ul 7+7, ascundem optiunile < 14 din select 2744$(document).on('shown.bs.tab', '#pills-7plus7-tab', function () { 2745 $('#pills-7plus7 .nights option').each(function () { 2746 if (parseInt($(this).val(), 10) < 14) { 2747 $(this).prop('disabled', true).hide(); 2748 } 2749 }); 2750 var $select = $('#pills-7plus7 .nights'); 2751 if (parseInt($select.val(), 10) < 14) { 2752 $select.val(14); 2753 } 2754}); 2755 2756// Mobil: la selectarea "7+7" din dropdown, afisam pack-content dar actualizam 2757// URL-ul si filtram optiunile de nopti la >= 14. 2758$('body').on('change', '.search-type-selector', function () { 2759 var val = $(this).val(); 2760 var $pack = $('#pack-content'); 2761 if (val === '7plus7') { 2762 $('.search-type-content').addClass('d-none'); 2763 $pack.removeClass('d-none'); 2764 2765 $pack.find('input[name="url"]').val('/rezultate-cautare-pachete-7plus7'); 2766 2767 // Asiguram un hidden "transport=7plus7" in form; daca nu exista il cream. 2768 var $form = $pack.find('form').first(); 2769 if ($form.find('input[name="transport"]').length) { 2770 $form.find('input[name="transport"]').val('7plus7'); 2771 } else { 2772 $form.prepend('<input type="hidden" name="transport" value="7plus7">'); 2773 } 2774 2775 // Clasa search-7plus7 ajuta packSuggestUrl sa returneze URL-urile filtrate 2776 $form.addClass('search-7plus7'); 2777 2778 $pack.find('.nights-radio-label').each(function () { 2779 var v = parseInt($(this).find('.nights-radio').val(), 10); 2780 $(this).toggleClass('d-none', v < 14); 2781 }); 2782 2783 var currentNights = parseInt($pack.find('input.nights').val(), 10); 2784 if (currentNights < 14) { 2785 $pack.find('.nights-radio[value="14"]').prop('checked', true); 2786 $pack.find('input.nights').val(14); 2787 $pack.find('.nights-preview').text('14 nopÈi'); 2788 $pack.find('.nights-radio-label').removeClass('border-primary bg-primary-subtle'); 2789 $pack.find('.nights-radio[value="14"]').closest('.nights-radio-label') 2790 .addClass('border-primary bg-primary-subtle'); 2791 } 2792 } else if (val === 'pack') { 2793 // Mod normal: URL standard, transport default "search", afisam doar [7, 14] 2794 $pack.find('input[name="url"]').val('/rezultate-cautare-pachete'); 2795 var $form2 = $pack.find('form').first(); 2796 if ($form2.find('input[name="transport"]').length) { 2797 $form2.find('input[name="transport"]').val('search'); 2798 } else { 2799 $form2.prepend('<input type="hidden" name="transport" value="search">'); 2800 } 2801 $form2.removeClass('search-7plus7'); 2802 $pack.find('.nights-radio-label').each(function () { 2803 var v = parseInt($(this).find('.nights-radio').val(), 10); 2804 $(this).toggleClass('d-none', v >= 21); 2805 }); 2806 } 2807}); 2808 2809 2810// ========================================================================== 2811// DEV-479 â Bilete de avion: cautare FARA data de plecare. 2812// Butonul âToate datele" din dropdown-ul de calendar (searchbox-charter-landing, 2813// folosit atat pe landing cat si pe pagina de rezultate) marcheaza explicit 2814// starea ânu conteaza data": scriem eticheta in inputul vizibil si golim ISO-urile 2815// (.start-at / .end-at) plus .nights. Backendul trateaza start_at gol ca âtoate 2816// plecarile catre destinatie" (FrontendSearchController@search_charters â 2817// charters.blade â Charter::availablePairs), deci nu mai trimitem o singura zi. 2818// NopÈile se golesc pentru ca fiecare pereche dus-intors are alta durata. 2819// ========================================================================== 2820$(document).on('click', '.flight-any-date-btn', function (e) { 2821 e.preventDefault(); 2822 e.stopPropagation(); 2823 2824 var $btn = $(this); 2825 var $searchBar = $btn.closest('.search-bar'); 2826 if (!$searchBar.length) { 2827 $searchBar = $btn.closest('.search-type-content:not(.d-none)'); 2828 } 2829 2830 var label = $btn.text().trim(); 2831 $searchBar.find('input.dates, input.dates-mobile').val(label); 2832 $searchBar.find('.start-at, .end-at').val(''); 2833 $searchBar.find('.nights').val(''); 2834 $searchBar.find('.calendar-preview').text(label); 2835 2836 // Inchidem dropdown-ul calendarului (pe mobil poate fi un sheet â atunci 2837 // butonul de dropdown nu exista si inchiderea o face âSalveaza"). 2838 var $toggle = $btn.closest('.dropdown').find('[data-bs-toggle="dropdown"]').first(); 2839 if ($toggle.length && window.bootstrap && bootstrap.Dropdown) { 2840 bootstrap.Dropdown.getOrCreateInstance($toggle[0]).hide(); 2841 2842 // DEV-557: ca la alegerea unei zile (suggestion-calendar), trecem direct la campul 2843 // urmator (Calatori). Dupa un tick: deschis in acelasi click, dropdown-ul ar fi inchis 2844 // imediat de handlerul global de click al Bootstrap. 2845 var $next = $btn.closest('.calendar-section').nextAll('.flex-grow-1').find('[data-bs-toggle="dropdown"]').first(); 2846 if ($next.length) { 2847 setTimeout(function () { bootstrap.Dropdown.getOrCreateInstance($next[0]).show(); }, 0); 2848 } 2849 } 2850});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.