PageSourceSearch

https://www.paralela45.ro/frontend/js/devteam-searchbox.js?v=80

js paralela45.ro collected 2026-10-02 12:55:38 UTC 126,639 bytes, 2,850 lines download raw bytes

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.