PageSourceSearch

https://rdutaxi.com/wp-content/plugins/ecab-taxi-booking-manager/a…/mptbm_registration.js?ver=1791584843

js rdutaxi.com collected 2026-10-09 22:28:25 UTC 255,077 bytes, 5,317 lines download raw bytes

1// The browser's back/forward cache (bfcache) can restore this page exactly
2// as it was in memory -- DOM, JS variables, everything -- without re-running
3// a request. That includes whatever search results/checkout markup were
4// injected via AJAX, with the mptbm_add_to_cart_nonce that was valid *then*.
5// Clicking Book Now on that restored page resubmits stale state (the earlier
6// Book Now click already emptied/rebuilt the cart), which can fail with no
7// visible error. event.persisted is true only when the page came from
8// bfcache, not a normal load -- this file is enqueued on every frontend page
9// (not just the booking one), so the reload is gated on the injected search-
10// result/checkout markup actually being present, rather than firing site-wide
11// on every back-navigation.
12window.addEventListener('pageshow', function (event) {
13    if (event.persisted && document.querySelector('.mptbm_map_search_result, .mptbm_order_summary')) {
14        window.location.reload();
15    }
16});
17
18// Admin/MPTBM_Settings_Global.php's 'use_shortest_route' Map API setting (read from
19// the #mptbm_use_shortest_route hidden field get_details.php renders, same pattern as
20// #mptbm_km_or_mile). Off by default: Google's own routes[0] pick balances time and
21// distance (generally what a driver actually navigates). When turned on, request
22// alternatives and price/display whichever route has the smallest distance instead -
23// see MPTBM_Function::get_server_distance() for the matching server-side behavior.
24function mptbm_use_shortest_route() {
25    var el = document.getElementById('mptbm_use_shortest_route');
26    return !!el && el.value === 'yes';
27}
28
29function mptbm_shortest_route_index(routes) {
30    var bestIndex = 0;
31    var bestDistance = null;
32    for (var i = 0; i < routes.length; i++) {
33        var legs = routes[i].legs || [];
34        var distance = 0;
35        for (var j = 0; j < legs.length; j++) {
36            distance += legs[j].distance.value;
37        }
38        if (bestDistance === null || distance < bestDistance) {
39            bestDistance = distance;
40            bestIndex = i;
41        }
42    }
43    return bestIndex;
44}
45
46let mptbm_map;
47let mptbm_map_window;
48var mptbm_start_marker = null;
49var mptbm_end_marker = null;
50var mptbm_extra_marker = null;
51
52// Collects the multi-row "Add Extra Stop" values (get_details.php's
53// .mptbm_extra_stops_wrapper) for the search-submit AJAX payloads below.
54// The legacy single #mptbm_map_extra_stop_place input no longer exists in
55// the markup (removed in favor of these rows), so this is the only working
56// source of extra-stop data - without it, jQuery's parent.find('#mptbm_map_extra_stop_place')
57// always resolves to an empty jQuery object and every request silently
58// submitted zero stops regardless of what the customer added.
59function mptbm_collect_extra_stop_places($scope) {
60    var places = [];
61    ($scope || jQuery(document)).find('.mptbm_extra_stop_place_input').each(function () {
62        var val = jQuery(this).val();
63        if (val) {
64            places.push(val);
65        }
66    });
67    return places;
68}
69
70function mptbm_collect_extra_stop_coordinates($scope) {
71    var coords = [];
72    ($scope || jQuery(document)).find('.mptbm_extra_stop_coords').each(function () {
73        var val = jQuery(this).val();
74        if (val) {
75            coords.push(val);
76        }
77    });
78    return coords;
79}
80
81// OpenStreetMap variables
82var mptbm_osm_map = null;
83var mptbm_osm_markers = [];
84var mptbm_osm_route = null;
85var mptbm_osm_start_marker = null;
86var mptbm_osm_end_marker = null;
87var mptbm_osm_extra_marker = null;
88var mptbm_manual_google_markers = [];
89var mptbm_manual_osm_markers = [];
90var mptbm_manual_google_markers_by_key = {};
91var mptbm_manual_osm_markers_by_key = {};
92var mptbm_manual_location_coordinates = {};
93var mptbm_manual_route_update_token = 0;
94
95// Per-pageload cache of address-search results, keyed by lowercased query
96// text -- re-typing something already searched (or the other field
97// matching the same text) resolves instantly with no network round trip.
98var mptbm_osm_search_cache = {};
99
100// Base Price global variables
101var mptbm_base_to_pickup_data = { distance: 0, duration: 0 };
102var mptbm_dropoff_to_base_data = { distance: 0, duration: 0 };
103
104function mptbm_calculate_base_distances(settings, pickup, dropoff, callback) {
105    if (!settings || !settings.coords || (!pickup && !dropoff)) {
106        if (callback) callback({ distance: 0, duration: 0 });
107        return;
108    }
109
110    var mapType = document.getElementById('mptbm_map_type');
111    var isOSM = mapType && mapType.value === 'openstreetmap';
112
113    if (isOSM) {
114        mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback);
115    } else {
116        mptbm_calculate_base_distances_google(settings, pickup, dropoff, callback);
117    }
118}
119
120function mptbm_calculate_base_distances_google(settings, pickup, dropoff, callback) {
121    if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
122        if (callback) callback({ distance: 0, duration: 0 });
123        return;
124    }
125
126    var service = new google.maps.DistanceMatrixService();
127    var origins = [];
128    var destinations = [];
129
130    // Charge Pickup: Base -> Pickup
131    if (settings.charge_pickup === 'yes' && pickup) {
132        origins.push(settings.coords);
133        destinations.push(pickup);
134    }
135
136    // Charge Dropoff: Dropoff -> Base
137    if (settings.charge_dropoff === 'yes' && dropoff) {
138        origins.push(dropoff);
139        destinations.push(settings.coords);
140    }
141
142    if (origins.length === 0) {
143        if (callback) callback({ distance: 0, duration: 0 });
144        return;
145    }
146
147    service.getDistanceMatrix({
148        origins: origins,
149        destinations: destinations,
150        travelMode: google.maps.TravelMode.DRIVING,
151        unitSystem: google.maps.UnitSystem.METRIC,
152    }, function (response, status) {
153        var result = { distance: 0, duration: 0 };
154        if (status === 'OK') {
155            var idx = 0;
156            // The results are in order of origins. Since each origin has one destination in our mapping:
157            // If we have both pickup and dropoff, origins[0] is Base, destinations[0] is Pickup.
158            // origins[1] is Dropoff, destinations[1] is Base.
159            // Wait, getDistanceMatrix returns a matrix (origins x destinations).
160            // This might result in 4 results if we pass [Base, Dropoff] and [Pickup, Base].
161            // We only need (Base, Pickup) and (Dropoff, Base).
162
163            // To keep it simple, let's just loop and pick the diagonal if we structured it right, 
164            // but DistanceMatrix is more like a grid.
165
166            // Better: use two separate requests or just parse the matrix correctly.
167            // Response.rows[i].elements[j]
168
169            if (settings.charge_pickup === 'yes' && pickup) {
170                var element = response.rows[idx].elements[idx];
171                if (element.status === 'OK') {
172                    result.distance += element.distance.value;
173                    result.duration += element.duration.value;
174                }
175                idx++;
176            }
177            if (settings.charge_dropoff === 'yes' && dropoff) {
178                var element = response.rows[idx].elements[idx];
179                if (element.status === 'OK') {
180                    result.distance += element.distance.value;
181                    result.duration += element.duration.value;
182                }
183            }
184        }
185        if (callback) callback(result);
186    });
187}
188
189function mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback) {
190    // For OSM, we need coordinates. If pickup/dropoff are names, we might need geocoding first.
191    // However, the plugin seems to handle coordinate selection for OSM.
192
193    var baseCoords = settings.coords.split(',');
194    var baseLat = baseCoords[0].trim();
195    var baseLng = baseCoords[1].trim();
196
197    // We'll use the OSRM API directly or via a proxy if needed.
198    // For now, let's assume we can use the same project OSRM as used in mptbm_calculate_osm_distance
199
200    var totalDistance = 0;
201    var totalDuration = 0;
202    var pending = 0;
203
204    function handleResult(data) {
205        if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
206            totalDistance += data.routes[0].distance;
207            totalDuration += data.routes[0].duration;
208        }
209        pending--;
210        if (pending === 0 && callback) {
211            callback({ distance: totalDistance, duration: totalDuration });
212        }
213    }
214
215    // Since OSRM handles point-to-point, we might need two calls if we don't want a single route.
216    // Actually, we can't easily geocode names in JS here without an API.
217    // Let's see if we have coordinates for pickup/dropoff.
218
219    var startCoords = window.mptbm_fixed_zone_start_coords; // If set by mptbm_handle_osm_address_selection
220    var endCoords = window.mptbm_fixed_zone_end_coords;
221
222    if (settings.charge_pickup === 'yes' && startCoords) {
223        pending++;
224        var url = 'https://router.project-osrm.org/route/v1/driving/' + baseLng + ',' + baseLat + ';' + startCoords.longitude + ',' + startCoords.latitude + '?overview=false';
225        fetch(url).then(r => r.json()).then(handleResult).catch(() => { pending--; if (pending === 0) callback({ distance: totalDistance, duration: totalDuration }); });
226    }
227
228    if (settings.charge_dropoff === 'yes' && endCoords) {
229        pending++;
230        var url = 'https://router.project-osrm.org/route/v1/driving/' + endCoords.longitude + ',' + endCoords.latitude + ';' + baseLng + ',' + baseLat + '?overview=false';
231        fetch(url).then(r => r.json()).then(handleResult).catch(() => { pending--; if (pending === 0) callback({ distance: totalDistance, duration: totalDuration }); });
232    }
233
234    if (pending === 0 && callback) {
235        callback({ distance: 0, duration: 0 });
236    }
237}
238
239// Function to clean up existing map instance
240function mptbm_cleanup_map() {
241    if (mptbm_map) {
242        // Clear any existing map instances
243        google.maps.event.clearInstanceListeners(mptbm_map);
244        mptbm_map = null;
245    }
246    if (mptbm_map_window) {
247        mptbm_map_window.close();
248        mptbm_map_window = null;
249    }
250}
251
252// Function to show location validation errors
253function showLocationError(element, message) {
254    // Remove any existing errors for this element
255    var existingError = element.parentElement.querySelector('.mptbm-location-error');
256    if (existingError) {
257        existingError.remove();
258    }
259
260    // Add error class to input
261    element.classList.add('mptbm-error-field');
262
263    // Create error message element
264    var errorDiv = document.createElement('div');
265    errorDiv.className = 'mptbm-location-error';
266    errorDiv.style.color = '#dc3545';
267    errorDiv.style.fontSize = '12px';
268    errorDiv.style.marginTop = '8px';
269    errorDiv.style.marginBottom = '10px';
270    errorDiv.textContent = message;
271
272    // Insert error message after the input
273    element.parentElement.appendChild(errorDiv);
274}
275
276// Function to remove all location errors
277function removeLocationErrors() {
278    var errorFields = document.querySelectorAll('.mptbm-error-field');
279    errorFields.forEach(function (field) {
280        field.classList.remove('mptbm-error-field');
281    });
282
283    var errorMessages = document.querySelectorAll('.mptbm-location-error');
284    errorMessages.forEach(function (error) {
285        error.remove();
286    });
287}
288
289function mptbm_resolve_redirect_url(response) {
290    if (!response) {
291        return '';
292    }
293
294    if (typeof response === 'object') {
295        if (response.redirect_url) {
296            return response.redirect_url;
297        }
298
299        if (response.data && response.data.redirect_url) {
300            return response.data.redirect_url;
301        }
302
303        return '';
304    }
305
306    if (typeof response === 'string') {
307        var cleaned = response.trim();
308
309        if (!cleaned) {
310            return '';
311        }
312
313        try {
314            var parsed = JSON.parse(cleaned);
315
316            if (typeof parsed === 'string') {
317                cleaned = parsed;
318            } else if (parsed && typeof parsed === 'object') {
319                return parsed.redirect_url || (parsed.data && parsed.data.redirect_url) || '';
320            }
321        } catch (error) {
322            // Keep the raw response when it is already a plain URL.
323        }
324
325        return cleaned
326            .replace(/^"+|"+$/g, '')
327            .replace(/\\\//g, '/');
328    }
329
330    return '';
331}
332
333// Add event listeners to clear errors when user starts typing
334jQuery(document).ready(function ($) {
335    // ---------------------------------------------------------------------
336    // Refresh the booking nonce for cached pages.
337    // Full-page caches serve logged-out visitors an HTML page whose embedded
338    // WordPress nonce expires after ~24h, so the search AJAX then fails with a
339    // 403/-1 ("works when logged in, fails when logged out"). admin-ajax.php is
340    // never full-page cached, so we pull a live nonce here and use it for every
341    // request. The user fills the pickup/dropoff/date form before searching, so
342    // this round-trip has completed long before the first search fires.
343    // ---------------------------------------------------------------------
344    window.mptbmNonceReady = (function () {
345        if (typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined') {
346            return $.Deferred().resolve().promise();
347        }
348        return $.ajax({
349            type: 'POST',
350            url: mp_ajax_url,
351            data: { action: 'mptbm_refresh_search_nonce' },
352            dataType: 'json'
353        }).done(function (res) {
354            if (res && res.success && res.data) {
355                if (res.data.search_nonce) {
356                    mptbm_ajax.search_nonce = res.data.search_nonce;
357                }
358                if (res.data.add_to_cart_nonce) {
359                    mptbm_ajax.add_to_cart_nonce = res.data.add_to_cart_nonce;
360                }
361            }
362        });
363    })();
364
365    // Clear errors on input for pickup location
366    $(document).on('input change', '#mptbm_map_start_place, #mptbm_manual_start_place', function () {
367        if (this.classList.contains('mptbm-error-field')) {
368            this.classList.remove('mptbm-error-field');
369            var errorMsg = this.parentElement.querySelector('.mptbm-location-error');
370            if (errorMsg) {
371                errorMsg.remove();
372            }
373        }
374    });
375
376    // Clear errors on input for dropoff location
377    $(document).on('input change', '#mptbm_map_end_place, #mptbm_manual_end_place', function () {
378        if (this.classList.contains('mptbm-error-field')) {
379            this.classList.remove('mptbm-error-field');
380            var errorMsg = this.parentElement.querySelector('.mptbm-location-error');
381            if (errorMsg) {
382                errorMsg.remove();
383            }
384        }
385    });
386
387    // Clear errors on input for extra stop location
388    $(document).on('input change', '#mptbm_map_extra_stop_place', function () {
389        if (this.classList.contains('mptbm-error-field')) {
390            this.classList.remove('mptbm-error-field');
391            var errorMsg = this.parentElement.querySelector('.mptbm-location-error');
392            if (errorMsg) {
393                errorMsg.remove();
394            }
395        }
396    });
397});
398
399function mptbm_set_cookie_distance_duration(start_place, end_place) {
400
401
402    // Check if OpenStreetMap is active
403    var mapType = document.getElementById('mptbm_map_type');
404
405    if (mapType && mapType.value === 'openstreetmap') {
406        return false;
407    }
408
409    // Safari compatibility: provide default values
410    start_place = start_place || "";
411    end_place = end_place || "";
412
413    // Check if map container exists before initializing
414    var mapContainer = document.getElementById("mptbm_map_area");
415    if (!mapContainer) {
416        return false;
417    }
418
419    // Check if Google Maps API is loaded
420    if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
421        return false;
422    }
423
424    // Only create a new map if one doesn't exist
425    if (!mptbm_map) {
426        mptbm_map = new google.maps.Map(mapContainer, {
427            mapTypeControl: false,
428            center: mp_lat_lng,
429            zoom: 15,
430        });
431    }
432
433    // Check if we have enough locations to calculate a route
434    // We need at least start_place and either end_place OR extra_stop
435    var extra_stop = jQuery('#mptbm_map_extra_stop_place').val();
436
437    // Multi-row "Add Extra Stop" inputs (get_details.php's
438    // .mptbm_extra_stops_wrapper) - read fresh each call, in DOM order, so
439    // added/edited/removed rows are always reflected without needing every
440    // caller of this function to be updated. Google's Directions API
441    // geocodes plain address text itself, so the input's value is enough -
442    // no need to touch the paired coordinate field here.
443    var multi_stop_addresses = [];
444    jQuery('.mptbm_extra_stop_place_input').each(function () {
445        var val = jQuery(this).val();
446        if (val) {
447            multi_stop_addresses.push(val);
448        }
449    });
450
451    if (start_place && (end_place || extra_stop || multi_stop_addresses.length)) {
452        var directionsService = new google.maps.DirectionsService();
453        var directionsRenderer = new google.maps.DirectionsRenderer();
454        directionsRenderer.setMap(mptbm_map);
455
456        // If dropoff isn't set yet, fall back to the last known stop (a
457        // multi-row stop if any exist, otherwise the legacy single extra
458        // stop) as a temporary destination so a route still draws.
459        var lastMultiStop = multi_stop_addresses.length ? multi_stop_addresses[multi_stop_addresses.length - 1] : '';
460        var actualDestination = end_place || lastMultiStop || extra_stop;
461        var useExtraAsWaypoint = (end_place || multi_stop_addresses.length) && extra_stop; // Only use legacy extra as waypoint if it's not the destination
462
463        var waypoints = [];
464        if (useExtraAsWaypoint) {
465            waypoints.push({
466                location: extra_stop,
467                stopover: true
468            });
469        }
470        multi_stop_addresses.forEach(function (address, index) {
471            // The last multi-stop is being used as the temporary destination
472            // above when dropoff isn't set yet - don't also list it as a waypoint.
473            if (!end_place && index === multi_stop_addresses.length - 1) {
474                return;
475            }
476            waypoints.push({
477                location: address,
478                stopover: true
479            });
480        });
481
482
483        var useShortestRoute = mptbm_use_shortest_route();
484        var request = {
485            origin: start_place,
486            destination: actualDestination,
487            waypoints: waypoints,
488            travelMode: google.maps.TravelMode.DRIVING,
489            unitSystem: google.maps.UnitSystem.METRIC,
490        };
491        if (useShortestRoute) {
492            request.provideRouteAlternatives = true;
493        }
494
495
496        var now = new Date();
497        var time = now.getTime();
498        var expireTime = time + 3600 * 1000 * 12;
499        now.setTime(expireTime);
500
501        // Safari compatibility: use function instead of arrow function
502        directionsService.route(request, function (result, status) {
503
504            if (status === google.maps.DirectionsStatus.OK) {
505                try {
506                    // Same route the server will price when useShortestRoute is on -
507                    // see mptbm_shortest_route_index() above. Otherwise routes[0], same
508                    // as Google's own default pick.
509                    var routeIndex = useShortestRoute ? mptbm_shortest_route_index(result.routes) : 0;
510
511                    // Sum all legs of the route (important when waypoints/extra 
511stops are used)
512                    var totalDistance = 0;
513                    var totalDuration = 0;
514                    var legs = result.routes[routeIndex].legs;
515
516
517                    for (var i = 0; i < legs.length; i++) {
518                        totalDistance += legs[i].distance.value;
519                        totalDuration += legs[i].duration.value;
520                    }
521
522
523                    var distance = totalDistance;
524                    var duration = totalDuration;
525
526                    var kmOrMileElement = document.getElementById("mptbm_km_or_mile");
527                    var kmOrMile = kmOrMileElement ? kmOrMileElement.value : 'km';
528                    var distance_text;
529                    var duration_text;
530
531                    if (kmOrMile == 'mile') {
532                        // Convert distance from meters to miles
533                        var distanceInKilometers = distance / 1000;
534                        var distanceInMiles = distanceInKilometers * 0.621371;
535                        distance_text = distanceInMiles.toFixed(1) + ' miles';
536                    } else {
537                        // Convert distance from meters to kilometers
538                        var distanceInKilometers = distance / 1000;
539                        distance_text = distanceInKilometers.toFixed(1) + ' km';
540                    }
541
542                    // Format duration (convert seconds to hours and minutes)
543                    var hours = Math.floor(duration / 3600);
544                    var minutes = Math.round((duration % 3600) / 60);
545                    if (hours > 0) {
546                        // duration_text = hours + ' hour' + (hours > 1 ? 's' : '') + ' min';
547                        duration_text = hours + ' hour' + (hours > 1 ? 's' : '') + ' ' + minutes + ' min';
548                    } else {
549                        duration_text = minutes + ' min';
550                    }
551
552                    // Safari compatibility: set cookies with proper encoding
553                    var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax";
554                    document.cookie = "mptbm_distance=" + encodeURIComponent(distance) + cookieOptions;
555                    document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions;
556                    document.cookie = "mptbm_duration=" + encodeURIComponent(duration) + cookieOptions;
557                    document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions;
558
559                    // Fallback: Update hidden fields for AJAX transmission (when cookies are blocked)
560                    var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area');
561                    if (mapArea.length > 0) {
562                        if (mapArea.find('input[name="mptbm_hidden_distance"]').length === 0) {
563                            mapArea.append('<input type="hidden" name="mptbm_hidden_distance" value="" />');
564                        }
565                        if (mapArea.find('input[name="mptbm_hidden_duration"]').length === 0) {
566                            mapArea.append('<input type="hidden" name="mptbm_hidden_duration" value="" />');
567                        }
568
569                        // Also update our explicit hidden fields if they exist
570                        var explicitDistance = document.getElementById('mptbm_calculated_distance');
571                        if (explicitDistance) {
572                            explicitDistance.value = distance;
573                        }
574                        var explicitDuration = document.getElementById('mptbm_calculated_duration');
575                        if (explicitDuration) {
576                            explicitDuration.value = duration;
577                        }
578
579                        // Add hidden inputs for text values as well (needed for cart display)
580                        if (mapArea.find('input[name="mptbm_hidden_distance_text"]').length === 0) {
581                            mapArea.append('<input type="hidden" name="mptbm_hidden_distance_text" value="" />');
582                        }
583                        if (mapArea.find('input[name="mptbm_hidden_duration_text"]').length === 0) {
584                            mapArea.append('<input type="hidden" name="mptbm_hidden_duration_text" value="" />');
585                        }
586
587                        mapArea.find('input[name="mptbm_hidden_distance"]').val(distance);
588                        mapArea.find('input[name="mptbm_hidden_duration"]').val(duration);
589                        mapArea.find('input[name="mptbm_hidden_distance_text"]').val(distance_text);
590                        mapArea.find('input[name="mptbm_hidden_duration_text"]').val(duration_text);
591                    }
592
593                    directionsRenderer.setDirections(result);
594                    if (useShortestRoute) {
595                        directionsRenderer.setRouteIndex(routeIndex);
596                    }
597
598                    // Update UI elements
599                    jQuery(".mptbm_total_distance").html(distance_text);
600                    jQuery(".mptbm_total_time").html(duration_text);
601                    jQuery(".mptbm_distance_time").slideDown("fast");
602                    mptbm_update_fixed_hours_warning();
603
604
605                } catch (error) {
606                    // Use fallback for Safari
607                    if (mptbm_is_safari()) {
608                        mptbm_fallback_distance_calculation(start_place, end_place);
609                    }
610                }
611            } else {
612
613                // Use fallback for Safari when API fails
614                if (mptbm_is_safari()) {
615                    mptbm_fallback_distance_calculation(start_place, end_place);
616                } else {
617                    // Show user-friendly error message for other browsers
618                    jQuery(".mptbm_total_distance").html("Error calculating distance");
619                    jQuery(".mptbm_total_time").html("Error calculating time");
620                    jQuery(".mptbm_distance_time").slideDown("fast");
621                }
622            }
623        });
624    } else if (start_place || end_place) {
625        var place = start_place ? start_place : end_place;
626        mptbm_map_window = new google.maps.InfoWindow();
627
628        // Check if map container exists before initializing
629        var mapContainer = document.getElementById("mptbm_map_area");
630        if (!mapContainer) {
631            console.warn("Map container #mptbm_map_area not found. Map initialization skipped.");
632            return false;
633        }
634
635        // Only create a new map if one doesn't exist
636        if (!mptbm_map) {
637            mptbm_map = new google.maps.Map(mapContainer, {
638                center: mp_lat_lng,
639                zoom: 15,
640            });
641        }
642
643        var request = {
644            query: place,
645            fields: ["name", "geometry"],
646        };
647
648        // Check if place is a coordinate string (Lat,Lng) to avoid INVALID_REQUEST from Places API
649        // Dictionary-style check or Regex
650        var coordPattern = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/;
651        if (coordPattern.test(place)) {
652            // It's a coordinate string. Manually create marker instead of calling Places API.
653            var parts = place.split(',');
654            var lat = parseFloat(parts[0]);
655            var lng = parseFloat(parts[1]);
656
657            if (!isNaN(lat) && !isNaN(lng)) {
658                var location = new google.maps.LatLng(lat, lng);
659                // Mock a place result object
660                var mockPlace = {
661                    geometry: { location: location },
662                    name: place
663                };
664                mptbmCreateMarker(mockPlace);
665                mptbm_map.setCenter(location);
666                return true;
667            }
668        }
669
670        var service = new google.maps.places.PlacesService(mptbm_map);
671        // Safari compatibility: use function instead of arrow function
672        service.findPlaceFromQuery(request, function (results, status) {
673
674            if (status === google.maps.places.PlacesServiceStatus.OK && results) {
675                for (var i = 0; i < results.length; i++) {
676                    mptbmCreateMarker(results[i]);
677                }
678                mptbm_map.setCenter(results[0].geometry.location);
679            } else {
680                console.error("Places API error:", status);
681            }
682        });
683    } else {
684        var directionsRenderer = new google.maps.DirectionsRenderer();
685        directionsRenderer.setMap(mptbm_map);
686        //document.getElementById('mptbm_map_start_place').focus();
687    }
688    return true;
689}
690function mptbmCreateMarker(place) {
691    if (!place.geometry || !place.geometry.location) return;
692
693    // Clear existing markers before adding new ones
694    if (mptbm_map && mptbm_map.markers) {
695        mptbm_map.markers.forEach(function (marker) {
696            marker.setMap(null);
697        });
698        mptbm_map.markers = [];
699    } else if (mptbm_map) {
700        mptbm_map.markers = [];
701    }
702
703    // Safari compatibility: use var instead of const
704    var marker = new google.maps.Marker({
705        map: mptbm_map,
706        position: place.geometry.location,
707    });
708
709    // Store marker reference for future clearing
710    if (mptbm_map) {
711        mptbm_map.markers = mptbm_map.markers || [];
712        mptbm_map.markers.push(marker);
713    }
714
715    // Safari compatibility: use function instead of arrow function
716    google.maps.event.addListener(marker, "click", function () {
717        if (mptbm_map_window) {
718            mptbm_map_window.setContent(place.name || "");
719            mptbm_map_window.open(mptbm_map);
720        }
721    });
722}
723// Every tab's template (Hourly, Distance, ...) renders its own
724// id="mptbm_map_area" div. Once a second tab has been loaded via AJAX, the
725// DOM holds more than one element sharing that id, so a bare
726// getElementById/querySelector always resolves to whichever one appears
727// first in the DOM -- typically the original (now hidden) tab, not the one
728// actually on screen. Scope the lookup to the visible tab when tabs exist,
729// and fall back to a bare lookup for the tabless [mptbm_booking] shortcode.
730function mptbm_get_current_map_area() {
731    var scoped = document.querySelector('.mptb-tab-content.current #mptbm_map_area');
732    return scoped || document.getElementById('mptbm_map_area');
733}
734
735function mptbm_get_current_map_wrap() {
736    var scoped = document.querySelector('.mptb-tab-content.current .mptbm_map_area');
737    return scoped || document.querySelector('.mptbm_map_area');
738}
739
740function mptbm_get_manual_map_locations() {
741    var mapWrap = mptbm_get_current_map_wrap();
742    if (!mapWrap || mapWrap.getAttribute('data-manual-map') !== 'yes') {
743        return [];
744    }
745
746    var dataElement = mapWrap.querySelector('.mptbm-manual-map-locations');
747    if (!dataElement) {
748        return [];
749    }
750
751    try {
752        var locations = JSON.parse(dataElement.textContent || '[]');
753        return Array.isArray(locations) ? locations : [];
754    } catch (error) {
755        console.warn('[Manual Route Map] Invalid location data.', error);
756        return [];
757    }
758}
759
760function mptbm_update_manual_map_status(resolved, total, complete) {
761    var mapWrap = mptbm_get_current_map_wrap();
762    var status = mapWrap ? mapWrap.querySelector('.mptbm_manual_map_status') : null;
763    if (!status) {
764        return;
765    }
766
767    if (complete) {
768        status.textContent = resolved + ' of ' + total + (total === 1 ? ' route location shown' : ' route locations shown');
769    } else {
770        status.textContent = 'Locating route points… ' + resolved + '/' + total;
771    }
772}
773
774function mptbm_pick_manual_osm_result(results, label) {
775    if (!Array.isArray(results) || !results.length) {
776        return null;
777    }
778
779    var expectedName = String(label || '').trim().toLowerCase();
780    var countryField = document.querySelector('[name="mptbm_country"]');
781    var expectedCountry = countryField && /^[a-z]{2}$/i.test(countryField.value) ? countryField.value.toLowerCase() : '';
782    var placeTypes = ['city', 'town', 'village', 'state', 'county', 'locality'];
783
784    return results.slice().sort(function (a, b) {
785        var score = function (result) {
786            var address = result.address || {};
787            var name = String(address.name || '').trim().toLowerCase();
788            var type = String(address.osm_value || address.type || '').toLowerCase();
789            var countryCode = String(address.countrycode || '').toLowerCase();
790            var value = 0;
791            if (expectedCountry && countryCode === expectedCountry) value += 200;
792            if (String(address.osm_key || '').toLowerCase() === 'place') value += 80;
793            if (placeTypes.indexOf(type) !== -1) value += 70;
794            if (name === expectedName) value += 50;
795            if (String(result.display_name || '').toLowerCase().indexOf(expectedName) === 0) value += 20;
796            return value;
797        };
798        return score(b) - score(a);
799    })[0];
800}
801
802function mptbm_render_manual_osm_locations() {
803    var locations = mptbm_get_manual_map_locations();
804    if (!locations.length || !mptbm_osm_map || typeof L === 'undefined') {
805        return;
806    }
807
808    mptbm_manual_osm_markers.forEach(function (marker) {
809        try { marker.remove(); } catch (error) { /* Marker may already belong to a removed map. */ }
810    });
811    mptbm_manual_osm_markers = [];
812    mptbm_manual_osm_markers_by_key = {};
813
814    var bounds = L.latLngBounds([]);
815    var resolved = 0;
816    var completed = 0;
817    var fitMap = function () {
818        if (bounds.isValid()) {
819            mptbm_osm_map.fitBounds(bounds, { padding: [45, 45], maxZoom: 11 });
820        }
821        mptbm_update_manual_map_status(resolved, locations.length, completed >= locations.length);
822    };
823    var addMarker = function (location, lat, lng) {
824        if (!isFinite(lat) || !isFinite(lng)) {
825            return;
826        }
827        var marker = L.marker([lat, lng], { title: location.label }).addTo(mptbm_osm_map);
828        marker.bindTooltip(location.label, {
829            permanent: true,
830            direction: 'top',
831            offset: [0, -8],
832            className: 'mptbm-manual-location-label'
833        });
834        marker.bindPopup('<strong>' + jQuery('<div>').text(location.label).html() + '</strong>');
835        mptbm_manual_osm_markers.push(marker);
836        mptbm_manual_osm_markers_by_key[location.key] = marker;
837        mptbm_manual_location_coordinates[location.key] = { latitude: lat, longitude: lng };
838        bounds.extend([lat, lng]);
839        resolved++;
840    };
841    var finishLocation = function () {
842        completed++;
843        fitMap();
844        if (completed >= locations.length) {
845            setTimeout(function () { mptbm_osm_map.invalidateSize(); fitMap(); }, 50);
846        }
847    };
848
849    locations.forEach(function (location) {
850        var savedLat = parseFloat(location.lat);
851        var savedLng = parseFloat(location.lng);
852        if (isFinite(savedLat) && isFinite(savedLng)) {
853            addMarker(location, savedLat, savedLng);
854            finishLocation();
855            return;
856        }
857
858        var query = String(location.label || '').trim();
859        var cacheKey = query.toLowerCase();
860        var cached = mptbm_osm_search_cache[cacheKey];
861        if (cached && cached.length) {
862            var cachedResult = mptbm_pick_manual_osm_result(cached, location.label);
863            addMarker(location, parseFloat(cachedResult.lat), parseFloat(cachedResult.lon));
864            finishLocation();
865            return;
866        }
867
868        jQuery.ajax({
869            url: mptbm_ajax.ajax_url,
870            method: 'GET',
871            data: { action: 'mptbm_osm_search', nonce: mptbm_ajax.osm_nonce, q: query }
872        }).done(function (response) {
873            var results = response && response.success && Array.isArray(response.data) ? response.data : [];
874            mptbm_osm_search_cache[cacheKey] = results;
875            if (results.length) {
876                var bestResult = mptbm_pick_manual_osm_result(results, location.label);
877                addMarker(location, parseFloat(bestResult.lat), parseFloat(bestResult.lon));
878            }
879        }).always(finishLocation);
880    });
881}
882
883function mptbm_render_manual_google_locations() {
884    var locations = mptbm_get_manual_map_locations();
885    var mapContainer = mptbm_get_current_map_area();
886    if (!locations.length || !mapContainer || typeof google === 'undefined' || !google.maps) {
887        return;
888    }
889
890    if (!mptbm_map || (typeof mptbm_map.getDiv === 'function' && mptbm_map.getDiv() !== mapContainer)) {
891        mptbm_map = new google.maps.Map(mapContainer, { center: mp_lat_lng, zoom: 7, mapTypeControl: false });
892    }
893    mptbm_manual_google_markers.forEach(function (marker) { marker.setMap(null); });
894    mptbm_manual_google_markers = [];
895    mptbm_manual_google_markers_by_key = {};
896
897    var geocoder = new google.maps.Geocoder();
898    var bounds = new google.maps.LatLngBounds();
899    var resolved = 0;
900    var completed = 0;
901    var addMarker = function (location, position) {
902        var marker = new google.maps.Marker({
903            map: mptbm_map,
904            position: position,
905            title: location.label,
906            label: { text: location.label, color: '#172033', fontSize: '12px', fontWeight: '700' }
907        });
908        var infoWindow = new google.maps.InfoWindow({ content: '<strong>' + jQuery('<div>').text(location.label).html() + '</strong>' });
909        marker.addListener('click', function () { infoWindow.open({ anchor: marker, map: mptbm_map }); });
910        mptbm_manual_google_markers.push(marker);
911        mptbm_manual_google_markers_by_key[location.key] = marker;
912        mptbm_manual_location_coordinates[location.key] = {
913            latitude: typeof position.lat === 'function' ? position.lat() : position.lat,
914            longitude: typeof position.lng === 'function' ? position.lng() : position.lng
915        };
916        bounds.extend(position);
917        resolved++;
918    };
919    var finishLocation = function () {
920        completed++;
921        if (!bounds.isEmpty()) {
922            mptbm_map.fitBounds(bounds, 45);
923        }
924        mptbm_update_manual_map_status(resolved, locations.length, completed >= locations.length);
925    };
926
927    locations.forEach(function (location) {
928        var savedLat = parseFloat(location.lat);
929        var savedLng = parseFloat(location.lng);
930        if (isFinite(savedLat) && isFinite(savedLng)) {
931            addMarker(location, { lat: savedLat, lng: savedLng });
932            finishLocation();
933            return;
934        }
935
936        var request = { address: location.label };
937        var restrictCountry = document.querySelector('[name="mptbm_restrict_search_country"]');
938        var country = document.querySelector('[name="mptbm_country"]');
939        if (restrictCountry && restrictCountry.value === 'yes' && country && country.value) {
940            request.componentRestrictions = { country: country.value };
941        }
942        geocoder.geocode(request, function (results, status) {
943            if (status === 'OK' && results && results[0]) {
944                addMarker(location, results[0].geometry.location);
945            }
946            finishLocation();
947        });
948    });
949}
950
951function mptbm_resolve_manual_location(locationKey, locationLabel) {
952    var deferred = jQuery.Deferred();
953    if (mptbm_manual_location_coordinates[locationKey]) {
954        deferred.resolve(mptbm_manual_location_coordinates[locationKey]);
955        return deferred.promise();
956    }
957
958    var mapType = document.getElementById('mptbm_map_type');
959    if (mapType && mapType.value === 'openstreetmap') {
960        var query = String(locationLabel || locationKey || '').trim();
961        var cacheKey = query.toLowerCase();
962        var useResults = function (results) {
963            var result = mptbm_pick_manual_osm_result(results, query
963);
964            if (!result) {
965                deferred.resolve(null);
966                return;
967            }
968            var coordinates = { latitude: parseFloat(result.lat), longitude: parseFloat(result.lon) };
969            mptbm_manual_location_coordinates[locationKey] = coordinates;
970            deferred.resolve(coordinates);
971        };
972        if (mptbm_osm_search_cache[cacheKey]) {
973            useResults(mptbm_osm_search_cache[cacheKey]);
974        } else {
975            jQuery.ajax({
976                url: mptbm_ajax.ajax_url,
977                method: 'GET',
978                data: { action: 'mptbm_osm_search', nonce: mptbm_ajax.osm_nonce, q: query }
979            }).done(function (response) {
980                var results = response && response.success && Array.isArray(response.data) ? response.data : [];
981                mptbm_osm_search_cache[cacheKey] = results;
982                useResults(results);
983            }).fail(function () { deferred.resolve(null); });
984        }
985    } else if (typeof google !== 'undefined' && google.maps) {
986        var request = { address: locationLabel || locationKey };
987        var country = document.querySelector('[name="mptbm_country"]');
988        if (country && /^[a-z]{2}$/i.test(country.value)) {
989            request.componentRestrictions = { country: country.value };
990        }
991        new google.maps.Geocoder().geocode(request, function (results, status) {
992            if (status !== 'OK' || !results || !results[0]) {
993                deferred.resolve(null);
994                return;
995            }
996            var position = results[0].geometry.location;
997            var coordinates = { latitude: position.lat(), longitude: position.lng() };
998            mptbm_manual_location_coordinates[locationKey] = coordinates;
999            deferred.resolve(coordinates);
1000        });
1001    } else {
1002        deferred.resolve(null);
1003    }
1004    return deferred.promise();
1005}
1006
1007function mptbm_update_manual_route_map($searchArea) {
1008    var mapWrap = mptbm_get_current_map_wrap();
1009    if (!mapWrap || mapWrap.getAttribute('data-manual-map') !== 'yes') {
1010        return;
1011    }
1012
1013    $searchArea = $searchArea && $searchArea.length ? $searchArea : jQuery(mapWrap).closest('.mptbm_transport_search_area');
1014    var $start = $searchArea.find('#mptbm_manual_start_place');
1015    var $end = $searchArea.find('#mptbm_manual_end_place');
1016    var startKey = $start.val();
1017    var endKey = $end.val();
1018    if (!startKey || !endKey) {
1019        return;
1020    }
1021
1022    var startLabel = $start.find('option:selected').data('label') || $start.find('option:selected').text();
1023    var endLabel = $end.find('option:selected').data('label') || $end.find('option:selected').text();
1024    var updateToken = ++mptbm_manual_route_update_token;
1025
1026    jQuery.when(
1027        mptbm_resolve_manual_location(startKey, startLabel),
1028        mptbm_resolve_manual_location(endKey, endLabel)
1029    ).done(function (startCoordinates, endCoordinates) {
1030        if (updateToken !== mptbm_manual_route_update_token || !startCoordinates || !endCoordinates) {
1031            return;
1032        }
1033
1034        $searchArea.data('mptbm-manual-start-coordinates', startCoordinates);
1035        $searchArea.data('mptbm-manual-end-coordinates', endCoordinates);
1036        var mapType = document.getElementById('mptbm_map_type');
1037        if (mapType && mapType.value === 'openstreetmap' && mptbm_osm_map) {
1038            mptbm_manual_osm_markers.forEach(function (marker) {
1039                var element = marker.getElement && marker.getElement();
1040                if (element) element.classList.remove('mptbm-manual-route-start', 'mptbm-manual-route-end');
1041            });
1042            mptbm_osm_start_marker = mptbm_manual_osm_markers_by_key[startKey] || L.marker([startCoordinates.latitude, startCoordinates.longitude]).addTo(mptbm_osm_map);
1043            mptbm_osm_end_marker = mptbm_manual_osm_markers_by_key[endKey] || L.marker([endCoordinates.latitude, endCoordinates.longitude]).addTo(mptbm_osm_map);
1044            var startElement = mptbm_osm_start_marker.getElement && mptbm_osm_start_marker.getElement();
1045            var endElement = mptbm_osm_end_marker.getElement && mptbm_osm_end_marker.getElement();
1046            if (startElement) startElement.classList.add('mptbm-manual-route-start');
1047            if (endElement) endElement.classList.add('mptbm-manual-route-end');
1048            mptbm_calculate_osm_distance();
1049        } else if (typeof google !== 'undefined' && google.maps && mptbm_map) {
1050            mptbm_start_marker = mptbm_manual_google_markers_by_key[startKey] || new google.maps.Marker({
1051                map: mptbm_map,
1052                position: { lat: startCoordinates.latitude, lng: startCoordinates.longitude },
1053                title: startLabel
1054            });
1055            mptbm_end_marker = mptbm_manual_google_markers_by_key[endKey] || new google.maps.Marker({
1056                map: mptbm_map,
1057                position: { lat: endCoordinates.latitude, lng: endCoordinates.longitude },
1058                title: endLabel
1059            });
1060            mptbm_calculate_google_route_from_markers();
1061        }
1062    });
1063}
1064
1065function mptbm_map_area_init() {
1066
1067    // Check if map container exists and is visible before initializing
1068    var mapContainer = mptbm_get_current_map_area();
1069    if (!mapContainer) {
1070        console.warn("[Map Init] Map container #mptbm_map_area not found. Skipping map initialization.");
1071        return false;
1072    }
1073
1074    // Check if the map container is visible (not hidden by CSS)
1075    var mapArea = mptbm_get_current_map_wrap();
1076    if (mapArea && mapArea.style.display === 'none') {
1077        // If map is hidden, we still want to initialize address search for OSM
1078        var mapType = document.getElementById('mptbm_map_type');
1079        if (mapType && mapType.value === 'openstreetmap') {
1080            mptbm_init_osm_address_search();
1081        }
1082        console.warn("[Map Init] Map area is hidden. Map rendering skipped, but address search may continue.");
1083        return false;
1084    }
1085
1086    // Check map type setting
1087    var mapType = document.getElementById('mptbm_map_type');
1088
1089    if (!mapType) {
1090        mapType = { value: 'enable' };
1091    }
1092
1093
1094    // Initialize based on map type
1095    if (mapType.value === 'openstreetmap') {
1096        return mptbm_init_osm_map();
1097    } else if (mapType.value === 'enable') {
1098        return mptbm_init_google_map();
1099    } else {
1100        return false;
1101    }
1102}
1103
1104// Safety net called right before the first marker is placed: if the map
1105// somehow isn't up yet (e.g. its container was hidden when
1106// mptbm_map_area_init() ran), build it lazily now instead of silently
1107// dropping the marker. A no-op once the map already exists.
1108function mptbm_ensure_osm_map_ready() {
1109    if (mptbm_osm_map) {
1110        return true;
1111    }
1112    var mapContainer = mptbm_get_current_map_area();
1113    if (!mapContainer) {
1114        return false;
1115    }
1116    mapContainer.innerHTML = '';
1117    return mptbm_init_osm_map();
1118}
1119
1120function mptbm_init_osm_map() {
1121
1122    if (typeof L === 'undefined') {
1123        return false;
1124    }
1125
1126    // Check if map container exists
1127    var mapContainer = mptbm_get_current_map_area();
1128    if (!mapContainer) {
1129        return false;
1130    }
1131
1132    // Clean up existing map instance if it exists
1133    if (mptbm_osm_map) {
1134        try {
1135            mptbm_osm_map.remove();
1136            mptbm_osm_map = null;
1137            mptbm_osm_markers = [];
1138            mptbm_osm_route = null;
1139            mptbm_osm_start_marker = null;
1140            mptbm_osm_end_marker = null;
1141            mptbm_osm_extra_marker = null;
1142        } catch (e) {
1143            console.log("[OSM] Error removing map:", e);
1144        }
1145    }
1146
1147
1148    // Get default coordinates from PHP or use fallback
1149    var defaultLat = (typeof mptbm_default_lat !== 'undefined') ? mptbm_default_lat : 40.7128;
1150    var defaultLng = (typeof mptbm_default_lng !== 'undefined') ? mptbm_default_lng : -74.0060;
1151
1152    // Initialize OpenStreetMap with configured coordinates. Passed as the
1153    // resolved element (not the 'mptbm_map_area' id string) since that id is
1154    // duplicated across tabs -- Leaflet would otherwise resolve it via its
1155    // own getElementById and land on the wrong tab's div.
1156    mptbm_osm_map = L.map(mapContainer).setView([defaultLat, defaultLng], 10);
1157
1158    // Add OpenStreetMap tiles
1159    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
1160        attribution: '© OpenStreetMap contributors'
1161    }).addTo(mptbm_osm_map);
1162
1163    // Initialize address search functionality
1164    mptbm_init_osm_address_search();
1165
1166    mptbm_render_manual_osm_locations();
1167
1168    return true;
1169}
1170
1171function mptbm_init_osm_address_search() {
1172
1173    // Clean up any existing autocomplete containers
1174    var existingContainers = document.querySelectorAll('.mptbm-osm-autocomplete');
1175    existingContainers.forEach(function (container) {
1176        container.remove();
1177    });
1178
1179    var startInput = document.getElementById('mptbm_map_start_place');
1180    var endInput = document.getElementById('mptbm_map_end_place');
1181
1182
1183    if (startInput) {
1184        mptbm_setup_osm_autocomplete(startInput, 'start');
1185    }
1186    if (endInput) {
1187        mptbm_setup_osm_autocomplete(endInput, 'end');
1188    }
1189    var extraInput = document.getElementById('mptbm_map_extra_stop_place');
1190    if (extraInput) {
1191        mptbm_setup_osm_autocomplete(extraInput, 'extra');
1192    }
1193}
1194
1195function mptbm_cleanup_osm_autocomplete(input) {
1196    if (!input || !input._mptbmOsmAutocomplete) {
1197        return;
1198    }
1199
1200    var state = input._mptbmOsmAutocomplete;
1201
1202    if (state.debounceTimer) {
1203        clearTimeout(state.debounceTimer);
1204    }
1205
1206    if (state.abortController) {
1207        state.abortController.abort();
1208    }
1209
1210    if (state.handlers) {
1211        input.removeEventListener('input', state.handlers.input);
1212        window.removeEventListener('scroll', state.handlers.positionDropdown);
1213        window.removeEventListener('resize', state.handlers.positionDropdown);
1214        document.removeEventListener('click', state.handlers.documentClick);
1215    }
1216
1217    if (state.container && state.container.parentNode) {
1218        state.container.parentNode.removeChild(state.container);
1219    }
1220
1221    delete input._mptbmOsmAutocomplete;
1222    input.removeAttribute('data-osm-autocomplete-initialized');
1223}
1224
1225function mptbm_setup_osm_autocomplete(input, type) {
1226    mptbm_cleanup_osm_autocomplete(input);
1227
1228    var autocompleteState = {
1229        abortController: null,
1230        container: null,
1231        currentSearchQuery: '',
1232        debounceTimer: null,
1233        handlers: null
1234    };
1235    var resultsContainer = document.createElement('div');
1236    resultsContainer.className = 'mptbm-osm-autocomplete';
1237    resultsContainer.setAttribute('data-autocomplete-type', type);
1238    resultsContainer.style.cssText = 'position: fixed; box-sizing: border-box; font-size:14px; background: #fff; border: 1px solid #e7eaf0; border-radius: 14px; max-height: 240px; overflow-y: auto; z-index: 99999 !important; display: none; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06); padding: 6px;';
1239
1240    // Append to body to avoid parent overflow issues
1241    document.body.appendChild(resultsContainer);
1242    autocompleteState.container = resultsContainer;
1243    input._mptbmOsmAutocomplete = autocompleteState;
1244
1245    // Mark input as initialized
1246    input.setAttribute('data-osm-autocomplete-initialized', 'true');
1247
1248    // Function to position the dropdown
1249    function positionDropdown() {
1250        var rect = input.getBoundingClientRect();
1251        // For fixed positioning, don't add scroll offset - use viewport coordinates directly
1252        var top = rect.bottom + 2;
1253        var left = rect.left;
1254        var width = rect.width;
1255
1256        resultsContainer.style.top = top + 'px';
1257        resultsContainer.style.left = left + 'px';
1258        resultsContainer.style.width = width + 'px';
1259
1260    }
1261
1262    function handleInput(e) {
1263        clearTimeout(autocompleteState.debounceTimer);
1264        var query = e.target.value.trim();
1265
1266        if (query.length < 3) {
1267            if (autocompleteState.abortController) {
1268                autocompleteState.abortController.abort();
1269                autocompleteState.abortController = null;
1270            }
1271            resultsContainer.style.display = 'none';
1272            autocompleteState.currentSearchQuery = '';
1273            return;
1274        }
1275
1276        // Store the current query
1277        autocompleteState.currentSearchQuery = query;
1278
1279        autocompleteState.debounceTimer = setTimeout(function () {
1280            positionDropdown();
1281            mptbm_search_osm_address(query, resultsContainer, input, type, autocompleteState.currentSearchQuery, autocompleteState);
1282        }, 300);
1283    }
1284
1285    input.addEventListener('input', handleInput);
1286
1287    // Reposition on scroll or resize
1288    window.addEventListener('scroll', positionDropdown);
1289    window.addEventListener('resize', positionDropdown);
1290
1291    // Hide results when clicking outside
1292    function handleDocumentClick(e) {
1293        if (e.target !== input && !resultsContainer.contains(e.target)) {
1294            resultsContainer.style.display = 'none';
1295        }
1296    }
1297
1298    document.addEventListener('click', handleDocumentClick);
1299
1300    autocompleteState.handlers = {
1301        input: handleInput,
1302        positionDropdown: positionDropdown,
1303        documentClick: handleDocumentClick
1304    };
1305}
1306
1307// Bangladesh-specific fallback matching, ported from the old server-side
1308// proxy (MPTBM_Dependencies::osm_search_proxy) -- Photon's country field is
1309// sometimes empty for BD results, so city/state name also counts as a match.
1310var MPTBM_BD_CITIES = ['DHAKA', 'CHITTAGONG', 'SYLHET', 'RAJSHAHI', 'KHULNA', 'BARISAL', 'RANGPUR', 'COMILLA', 'NARAYANGANJ', 'GAZIPUR'];
1311var MPTBM_BD_STATES = ['DHAKA', 'CHITTAGONG', 'SYLHET', 'RAJSHAHI', 'KHULNA', 'BARISAL', 'RANGPUR', 'DIVISION'];
1312
1313// Same GeoJSON -> {display_name, lat, lon} shape and country filtering the
1314// PHP proxy used to do server-side, ported 1:1 so behavior is unchanged now
1315// that the call happens directly from the browser.
1316function mptbm_transform_photon_results(geojson, restrictToCountry, countryCode) {
1317    var results = [];
1318    if (!geojson || !Array.isArray(geojson.features)) {
1319        return results;
1320    }
1321
1322    geojson.features.forEach(function (feature) {
1323        var props = feature.properties || {};
1324        var coords = (feature.geometry && feature.geometry.coordinates) || [];
1325
1326        if (restrictToCountry && countryCode) {
1327            var featureCountry = (props.countrycode || '').toUpperCase();
1328            var featureCountryName = (props.country || '').toUpperCase();
1329            var featureState = (props.state || '').toUpperCase();
1330            var featureCity = (props.city || '').toUpperCase();
1331            var matches = false;
1332
1333            if (featureCountry && featureCountry === countryCode) {
1334                matches = true;
1335            }
1336
1337            if (!matches && countryCode === 'BD') {
1338                if (featureCountryName === 'BANGLADESH' || featureCountryName === 'BD') {
1339                    matches = true;
1340                }
1341                if (!matches && MPTBM_BD_CITIES.indexOf(featureCity) !== -1) {
1342                    matches = true;
1343                } else if (!matches && MPTBM_BD_STATES.indexOf(featureState) !== -1) {
1344                    matches = true;
1345                }
1346            }
1347
1348            // No country info at all on this result -- allow it through
1349            // rather than silently dropping it.
1350            if (!featureCountry && !featureCountryName && !featureCity && !featureState) {
1351                matches = true;
1352            }
1353
1354            if (!matches) {
1355                return;
1356            }
1357        }
1358
1359        // Place name + city is enough to identify a pickup/drop-off for a
1360        // service operating in one city/country - state and country were
1361        // repeated on every single result (e.g. "..., Dhaka Division,
1362        // Bangladesh") and made addresses needlessly long throughout the
1363        // booking flow (summary, admin bookings list, order emails, ...).
1364        // Only fall back to the broader state/country if neither a specific
1365        // name nor a city came back at all, so a legitimately resolvable
1366        // rural result still shows something instead of "Unknown Location".
1367        var nameParts = [props.name, props.city].filter(Boolean);
1368        if (!nameParts.length) {
1369            nameParts = [props.state, props.country].filter(Boolean);
1370        }
1371
1372        results.push({
1373            display_name: nameParts.length ? nameParts.join(', ') : 'Unknown Location',
1374            lat: coords.length > 1 ? coords[1] : 0,
1375            lon: coords.length > 0 ? coords[0] : 0
1376        });
1377    });
1378
1379    return results;
1380}
1381
1382function mptbm_render_osm_search_results(results, container, input, type) {
1383    container.innerHTML = '';
1384
1385    if (!results || results.length === 0) {
1386        container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 600;">No results found</div>';
1387        container.style.display = 'block';
1388        return;
1389    }
1390
1391    results.forEach(function (result) {
1392        var item = document.createElement('div');
1393        item.style.cssText = 'display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin: 2px 0; cursor: pointer; border-radius: 10px; color: #0f172a; font-size: 13.5px; font-weight: 500; line-height: 1.4; transition: background-color .15s ease;';
1394
1395        var icon = document.createElement('i');
1396        icon.className = 'fas fa-map-marker-alt';
1397        icon.style.cssText = 'flex: 0 0 auto; width: 14px; margin-top: 2px; color: #94a3b8; font-size: 13px;';
1398
1399        var text = document.createElement('span');
1400        text.textContent = result.display_name;
1401        text.style.cssText = 'flex: 1 1 auto; min-width: 0;';
1402
1403        item.appendChild(icon);
1404        item.appendChild(text);
1405
1406        item.addEventListener('click', function () {
1407            input.value = result.display_name;
1408            container.style.display = 'none';
1409            mptbm_handle_osm_address_selection(result, type);
1410        });
1411
1412        item.addEventListener('mouseenter', function () {
1413            this.style.backgroundColor = '#f1f4f9';
1414        });
1415
1416        item.addEventListener('mouseleave', function () {
1417            this.style.backgroundColor = 'transparent';
1418        });
1419
1420        container.appendChild(item);
1421    });
1422
1423    container.style.display = 'block';
1424}
1425
1426// Calls Photon directly from the browser instead of proxying through
1427// admin-ajax.php. The proxy added a full WordPress bootstrap plus an
1428// outbound request from the server itself on every keystroke search --
1429// on hosts where the server's own outbound connection is slow (the same
1430// class of issue as a cURL timeout to any other external API), that hop
1431// alone could make autocomplete take several seconds per query. Calling
1432// Photon straight from the visitor's browser removes that hop entirely;
1433// Photon's public API already supports CORS for exactly this use.
1434function mptbm_search_osm_address(query, container, input, type, expectedQuery, autocompleteState, retryCount) {
1435    retryCount = retryCount || 0;
1436    var cacheKey = query.toLowerCase();
1437    var cached = mptbm_osm_search_cache[cacheKey];
1438    if (cached) {
1439        mptbm_render_osm_search_results(cached, container, input, type);
1440        return;
1441    }
1442
1443    container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 600;">Searching&hellip;</div>';
1444    container.style.display = 'block';
1445
1446    var restrictEl = document.getElementById('mptbm_restrict_search_country');
1447    var countryEl = document.getElementById('mptbm_country');
1448    var restrictToCountry = !!restrictEl && restrictEl.value === 'yes';
1449    var countryCode = (countryEl && countryEl.value ? countryEl.value : '').toUpperCase();
1450
1451    var abortController = null;
1452    var timeoutId = null;
1453    if (autocompleteState && typeof AbortController !== 'undefined') {
1454        if (autocompleteState.abortController) {
1455            autocompleteState.abortController.abort();
1456        }
1457
1458        abortController = new AbortController();
1459        autocompleteState.abortController = abortController;
1460        // Photon is a shared public instance -- bound the wait instead of
1461        // leaving "Searching..." on screen indefinitely if it stalls.
1462        timeoutId = setTimeout(function () {
1463            abortController.abort();
1464        }, 8000);
1465    }
1466
1467    var url = 'https://photon.komoot.io/api/?q=' + encodeURIComponent(query) + '&limit=5&lang=en';
1468
1469    fetch(url, {
1470        signal: abortController ? abortController.signal : undefined
1471    })
1472        .then(function (response) {
1473            return response.json();
1474        })
1475        .then(function (geojson) {
1476            if (timeoutId) {
1477                clearTimeout(timeoutId);
1478            }
1479            if (autocompleteState && autocompleteState.abortController === abortController) {
1480                autocompleteState.abortController = null;
1481            }
1482
1483            // Check if this response is still relevant (user hasn't typed more)
1484            var currentValue = input.value.trim();
1485            if (expectedQuery && currentValue !== expectedQuery) {
1486                return;
1487            }
1488
1489            var results = mptbm_transform_photon_results(geojson, restrictToCountry, countryCode);
1490            mptbm_osm_search_cache[cacheKey] = results;
1491            mptbm_render_osm_search_results(results, container, input, type);
1492        })
1493        .catch(function (error) {
1494            if (timeoutId) {
1495                clearTimeout(timeoutId);
1496            }
1497            if (autocompleteState && autocompleteState.abortController === abortController) {
1498                autocompleteState.abortController = null;
1499            }
1500
1501            if (error && error.name === 'AbortError') {
1502                return;
1503            }
1504
1505            // fetch() collapses every network-layer failure (DNS hiccup, a
1506            // VPN/Wi-Fi adapter flapping mid-request -- ERR_NETWORK_CHANGED,
1507            // a dropped connection, ...) into this same generic "Failed to
1508            // fetch" TypeError with no way to tell them apart. Most of those
1509            // are transient and gone a moment later, so retry once
1510            // automatically before bothering the visitor with an error --
1511            // only if they haven't since typed something else.
1512            console.error('[OSM Search] Fetch error:', error);
1513            if (retryCount < 1 && input.value.trim() === expectedQuery) {
1514                setTimeout(function () {
1515                    if (input.value.trim() === expectedQuery) {
1516                        mptbm_search_osm_address(query, container, input, type, expectedQuery, autocompleteState, retryCount + 1);
1517                    }
1518                }, 700);
1519                return;
1520            }
1521
1522            container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #dc2626; font-size: 13px; font-weight: 600;">Search failed. Please try again.</div>';
1523            container.style.display = 'block';
1524        });
1525}
1526
1527function mptbm_handle_osm_address_selection(address, type) {
1528    var lat = parseFloat(address.lat);
1529    var lng = parseFloat(address.lon);
1530    var price_based = jQuery('[name="mptbm_price_based"]').val();
1531
1532    // First address picked: swap the idle Google preview for the real map.
1533    mptbm_ensure_osm_map_ready();
1534
1535    // Remove existing marker for this type
1536    if (type === 'start' && mptbm_osm_start_marker) {
1537        mptbm_osm_map.removeLayer(mptbm_osm_start_marker);
1538    } else if (type === 'end' && mptbm_osm_end_marker) {
1539        mptbm_osm_map.removeLayer(mptbm_osm_end_marker);
1540    } else if (type === 'extra' && mptbm_osm_extra_marker) {
1541        mptbm_osm_map.removeLayer(mptbm_osm_extra_marker);
1542    }
1543
1544    // Create new marker if map exists
1545    if (mptbm_osm_map) {
1546        var marker = L.marker([lat, lng]).addTo(mptbm_osm_map);
1547        marker.bindPopup(address.display_name);
1548
1549        if (type === 'start') {
1550            mptbm_osm_start_marker = marker;
1551            window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng };
1552            // Remembers exactly which address text these coordinates belong
1553            // to, so the Search button can reuse them instead of re-geocoding
1554            // the same text again -- see getCachedOrFreshCoordinates().
1555            window.mptbm_osm_start_coords_address = address.display_name;
1556        } else if (type === 'end') {
1557            mptbm_osm_end_marker = marker;
1558            window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng };
1559            window.mptbm_osm_end_coords_address = address.display_name;
1560        } else if (type === 'extra') {
1561            mptbm_osm_extra_marker = marker;
1562        }
1563
1564        // Calculate distance if we have start marker and either end marker OR extra marker
1565        if (mptbm_osm_start_marker && (mptbm_osm_end_marker || mptbm_osm_extra_marker)) {
1566            mptbm_calculate_osm_distance();
1567        }
1568
1569        // Fit map to show all markers
1570        var markersToFit = [mptbm_osm_start_marker, mptbm_osm_end_marker];
1571        if (mptbm_osm_extra_marker) markersToFit.push(mptbm_osm_extra_marker);
1572
1573        var group = new L.featureGroup(markersToFit.filter(Boolean));
1574        if (group.getLayers().length > 0) {
1575            mptbm_osm_map.fitBounds(group.getBounds().pad(0.1));
1576        }
1577    }
1578}
1579
1580// Warn the user when the selected "Select Hours" rental duration is shorter
1581// than the estimated drive time for the chosen pickup/drop-off route.
1582function mptbm_update_fixed_hours_warning() {
1583    var priceBasedEl = document.querySelector('[name="mptbm_price_based"]');
1584    if (!priceBasedEl || priceBasedEl.value !== 'fixed_hourly') return;
1585
1586    var warningEl = document.getElementById('mptbm_fixed_hours_warning');
1587    var hoursEl = document.getElementById('mptbm_fixed_hours');
1588    var durationEl = document.getElementById('mptbm_calculated_duration');
1589    if (!warningEl || !hoursEl || !durationEl) return;
1590
1591    var durationSeconds = parseFloat(durationEl.value) || 0;
1592    var selectedHours = parseInt(hoursEl.value, 10) || 0;
1593
1594    if (durationSeconds <= 0) {
1595        warningEl.style.display = 'none';
1596        return;
1597    }
1598
1599    var requiredHours = Math.max(1, Math.ceil(durationSeconds / 3600));
1600
1601    if (requiredHours > selectedHours) {
1602        var totalMinutes = Math.round(durationSeconds / 60);
1603        var h = Math.floor(totalMinutes / 60);
1604        var m = totalMinutes % 60;
1605        var tripTimeText = h > 0 ? (h + 'h' + (m > 0 ? ' ' + m + 'm' : '')) : m + 'm';
1606        var hourWord = requiredHours === 1 ? 'hour' : 'hours';
1607
1608        var textEl = warningEl.querySelector('span');
1609        if (textEl) {
1610            textEl.textContent = 'Estimated trip time is ~' + tripTimeText + '. Select at least ' + requiredHours + ' ' + hourWord + ' to cover this trip.';
1611        }
1612        warningEl.style.display = 'flex';
1613    } else {
1614        warningEl.style.display = 'none';
1615    }
1616}
1617
1618jQuery(document).on('mp_change change', '#mptbm_fixed_hours', function () {
1619    mptbm_update_fixed_hours_warning();
1620});
1621
1622// Custom-styled dropdown proxy for Transfer Type / Extra Waiting Hours.
1623// A real (visually hidden) <select> stays in the DOM so existing behaviour
1624// (the Return-date/time collapse toggle, price refresh listeners) keeps
1625// working untouched -- clicking a list item just updates that select and
1626// fires a native "change" event on it.
1627(function ($) {
1628    "use strict";
1629
1630    $(document).on('click', '.mptbm_select_proxy input.formControl', function (e) {
1631        e.stopPropagation();
1632        var $list = $(this).closest('.mptbm_select_proxy').find('.mp_input_select_list');
1633        var isOpen = $list.is(':visible');
1634        $('.mptbm_select_proxy .mp_input_select_list').not($list).slide
1634Up(200);
1635        $list[isOpen ? 'slideUp' : 'slideDown'](200);
1636    });
1637
1638    $(document).on('click', '.mptbm_select_proxy .mp_input_select_list li', function (e) {
1639        e.preventDefault();
1640        var $li = $(this);
1641        var $wrapper = $li.closest('.mptbm_select_proxy');
1642
1643        $wrapper.find('input.formControl').val($li.text());
1644        $wrapper.find('select.mptbm_proxy_native_select').val($li.data('value')).trigger('change');
1645        $wrapper.find('.mp_input_select_list').slideUp(200);
1646    });
1647
1648    $(document).on('click', function (e) {
1649        if (!$(e.target).closest('.mptbm_select_proxy').length) {
1650            $('.mptbm_select_proxy .mp_input_select_list').slideUp(200);
1651        }
1652    });
1653})(jQuery);
1654
1655function mptbm_calculate_osm_distance() {
1656    // Multi-row "Add Extra Stop" coordinates (get_details.php's
1657    // .mptbm_extra_stops_wrapper), read fresh in DOM order so added/edited/
1658    // removed rows are always reflected. OSRM needs actual coordinates
1659    // (unlike Google's Directions API, which can geocode address text
1660    // itself), which is exactly what the paired .mptbm_extra_stop_coords
1661    // hidden field stores ("lat,lng") once a suggestion is picked.
1662    var multiStopCoords = [];
1663    jQuery('.mptbm_extra_stop_coords').each(function () {
1664        var val = jQuery(this).val();
1665        if (!val) return;
1666        var parts = val.split(',');
1667        if (parts.length === 2) {
1668            var lat = parseFloat(parts[0]);
1669            var lng = parseFloat(parts[1]);
1670            if (!isNaN(lat) && !isNaN(lng)) {
1671                multiStopCoords.push({ lat: lat, lng: lng });
1672            }
1673        }
1674    });
1675
1676    // We need at least start marker and either end marker, extra marker, or a multi-stop
1677    if (!mptbm_osm_start_marker || (!mptbm_osm_end_marker && !mptbm_osm_extra_marker && !multiStopCoords.length)) return;
1678
1679    var startLatLng = mptbm_osm_start_marker.getLatLng();
1680
1681    // Use end marker if available; otherwise fall back to the last known
1682    // stop (a multi-row stop if any exist, else the legacy single extra
1683    // marker) as a temporary destination so a route still draws.
1684    var endLatLng;
1685    if (mptbm_osm_end_marker) {
1686        endLatLng = mptbm_osm_end_marker.getLatLng();
1687    } else if (multiStopCoords.length) {
1688        var lastStop = multiStopCoords.pop();
1689        endLatLng = L.latLng(lastStop.lat, lastStop.lng);
1690    } else {
1691        endLatLng = mptbm_osm_extra_marker.getLatLng();
1692    }
1693
1694    // Determine if we should use the legacy single extra marker as a
1695    // waypoint (only if it isn't itself being used as the destination above)
1696    var useExtraAsWaypoint = (mptbm_osm_end_marker || multiStopCoords.length) && mptbm_osm_extra_marker;
1697
1698    var urlCoords = startLatLng.lng + ',' + startLatLng.lat;
1699
1700    if (useExtraAsWaypoint) {
1701        var extraLatLng = mptbm_osm_extra_marker.getLatLng();
1702        urlCoords += ';' + extraLatLng.lng + ',' + extraLatLng.lat;
1703    }
1704
1705    multiStopCoords.forEach(function (coord) {
1706        urlCoords += ';
1706' + coord.lng + ',' + coord.lat;
1707    });
1708
1709    urlCoords += ';' + endLatLng.lng + ',' + endLatLng.lat;
1710
1711    // Get route from OSRM (Open Source Routing Machine)
1712    var osrmUrl = 'https://router.project-osrm.org/route/v1/driving/' +
1713        urlCoords +
1714        '?overview=full&geometries=geojson';
1715
1716    fetch(osrmUrl)
1717        .then(response => response.json())
1718        .then(data => {
1719
1720            if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
1721                var route = data.routes[0];
1722                var distanceInMeters = route.distance; // Distance in meters
1723                var durationInSeconds = route.duration; // Duration in seconds
1724                var distance = distanceInMeters / 1000; // Convert meters to km
1725                var duration = durationInSeconds / 3600; // Convert seconds to hours
1726
1727
1728                // Prepare cookie data
1729                var kmOrMile = document.getElementById('mptbm_km_or_mile').value;
1730                var distance_text, display_distance;
1731
1732                if (kmOrMile === 'mile') {
1733                    // Convert to miles
1734                    var distanceInMiles = distance * 0.621371;
1735                    distance_text = distanceInMiles.toFixed(1) + ' miles';
1736                    display_distance = ' ' + distanceInMiles.toFixed(1) + ' MILE';
1737                } else {
1738                    distance_text = distance.toFixed(1) + ' km';
1739                    display_distance = ' ' + distance.toFixed(1) + ' KM';
1740                }
1741
1742                // Format duration text
1743                var hours = Math.floor(duration);
1744                var minutes = Math.round((duration - hours) * 60);
1745                var duration_text;
1746                if (hours > 0) {
1747                    duration_text = hours + ' Hour ' + minutes + ' Min';
1748                } else {
1749                    duration_text = minutes + ' Min';
1750                }
1751
1752                // Set cookies for price calculation (same format as Google Maps)
1753                var now = new Date();
1754                now.setTime(now.getTime() + (24 * 60 * 60 * 1000)); // 24 hours
1755                var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax";
1756
1757                document.cookie = "mptbm_distance=" + encodeURIComponent(distanceInMeters) + cookieOptions;
1758                document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions;
1759                document.cookie = "mptbm_duration=" + encodeURIComponent(durationInSeconds) + cookieOptions;
1760                document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions;
1761
1762                // Update explicit hidden fields
1763                var explicitDistance = document.getElementById('mptbm_calculated_distance');
1764                if (explicitDistance) {
1765                    explicitDistance.value = distanceInMeters;
1766                }
1767                var explicitDuration = document.getElementById('mptbm_calculated_duration');
1768                if (explicitDuration) {
1769                    explicitDuration.value = durationInSeconds;
1770                }
1771
1772
1773                // Update distance display
1774                var currentMapWrap = mptbm_get_current_map_wrap();
1775                var distanceElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_distance') : null;
1776                if (distanceElement) {
1777                    distanceElement.textContent = display_distance;
1778                }
1779
1780                // Update time display
1781                var timeElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_time') : null;
1782                if (timeElement) {
1783                    timeElement.textContent = duration_text;
1784                }
1785
1786                // Show distance/time section
1787                jQuery(currentMapWrap).find('.mptbm_distance_time').slideDown('fast');
1788                mptbm_update_fixed_hours_warning();
1789
1790                // The numbers above came from OSRM, called straight from this browser.
1791                // The fare, however, is measured on the server - which may be configured
1792                // to use a different routing service entirely (Map API Settings > Routing
1793                // Service), because OSRM can only route over roads present in
1794                // OpenStreetMap and detours around the ones that are missing. Whenever
1795                // the two disagree the customer would read one distance here and be
1796                // charged for another, so ask the server what it measured and let that
1797                // win. The polyline below still comes from OSRM: it is only the drawn
1798                // shape, and the figures are what the fare is built on.
1799                mptbm_sync_distance_from_server(startLatLng, endLatLng);
1800
1801                // Draw route on map
1802                if (mptbm_osm_route) {
1803                    mptbm_osm_map.removeLayer(mptbm_osm_route);
1804                }
1805
1806                // Convert GeoJSON coordinates to Leaflet format [lat, lng]
1807                var coordinates = route.geometry.coordinates.map(function (coord) {
1808                    return [coord[1], coord[0]]; // GeoJSON uses [lng, lat], Leaflet uses [lat, lng]
1809                });
1810
1811                var isManualRoute = currentMapWrap && currentMapWrap.getAttribute('data-manual-map') === 'yes';
1812                mptbm_osm_route = L.polyline(coordinates, {
1813                    color: isManualRoute ? '#059669' : '#ff4757',
1814                    weight: isManualRoute ? 6 : 4,
1815                    opacity: 0.9
1816                }).addTo(mptbm_osm_map);
1817
1818                // Fit map to show the entire route
1819                mptbm_osm_map.fitBounds(mptbm_osm_route.getBounds().pad(0.1));
1820
1821            } else {
1822                console.error('[OSM Route] No route found');
1823                // Fallback to straight line
1824                drawStraightLine(startLatLng, endLatLng);
1825            }
1826        })
1827        .catch(error => {
1828            console.error('[OSM Route] Error fetching route:', error);
1829            // Fallback to straight line
1830            drawStraightLine(startLatLng, endLatLng);
1831        });
1832
1833    // Fallback function to draw straight line
1834    function drawStraightLine(start, end) {
1835        var distance = mptbm_osm_map.distance(start, end) / 1000;
1836
1837        var currentMapWrap = mptbm_get_current_map_wrap();
1838        var distanceElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_distance') : null;
1839        if (distanceElement) {
1840            var kmOrMile = document.getElementById('mptbm_km_or_mile').value;
1841            if (kmOrMile === 'mile') {
1842                distance = distance * 0.621371;
1843                distanceElement.textContent = ' ' + distance.toFixed(1) + ' MILE';
1844            } else {
1845                distanceElement.textContent = ' ' + distance.toFixed(1) + ' KM';
1846            }
1847        }
1848
1849        if (mptbm_osm_route) {
1850            mptbm_osm_map.removeLayer(mptbm_osm_route);
1851        }
1852
1853        mptbm_osm_route = L.polyline([start, end], {
1854            color: '#ff4757',
1855            weight: 4,
1856            opacity: 0.8,
1857            dashArray: '10, 10' // Dashed to show it's straight line
1858        }).addTo(mptbm_osm_map);
1859    }
1860}
1861
1862function mptbm_calculate_google_route_from_markers() {
1863    if (!mptbm_start_marker || !mptbm_end_marker || !mptbm_map) return;
1864
1865    var directionsService = new google.maps.DirectionsService();
1866    var directionsRenderer = new google.maps.DirectionsRenderer();
1867    directionsRenderer.setMap(mptbm_map);
1868
1869    var useShortestRoute = mptbm_use_shortest_route();
1870    var request = {
1871        origin: mptbm_start_marker.getPosition(),
1872        destination: mptbm_end_marker.getPosition(),
1873        travelMode: google.maps.TravelMode.DRIVING,
1874        unitSystem: google.maps.UnitSystem.METRIC,
1875    };
1876    if (useShortestRoute) {
1877        request.provideRouteAlternatives = true;
1878    }
1879
1880    directionsService.route(request, function (result, status) {
1881        if (status === google.maps.DirectionsStatus.OK) {
1882            try {
1883                // Same route the server will price when useShortestRoute is on - see
1884                // mptbm_shortest_route_index() above.
1885                var routeIndex = useShortestRoute ? mptbm_shortest_route_index(result.routes) : 0;
1886                var distance = result.routes[routeIndex].legs[0].distance.value;
1887                var kmOrMileElement = document.getElementById("mptbm_km_or_mile");
1888                var kmOrMile = kmOrMileElement ? kmOrMileElement.value : 'km';
1889                var distance_text = result.routes[routeIndex].legs[0].distance.text;
1890                var duration = result.routes[routeIndex].legs[0].duration.value;
1891                var duration_text = result.routes[routeIndex].legs[0].duration.text;
1892
1893                if (kmOrMile == 'mile') {
1894                    var distanceInKilometers = distance / 1000;
1895                    var distanceInMiles = distanceInKilometers * 0.621371;
1896                    distance_text = distanceInMiles.toFixed(1) + ' miles';
1897                }
1898
1899                var now = new Date();
1900                var time = now.getTime();
1901                var expireTime = time + 3600 * 1000 * 12;
1902                now.setTime(expireTime);
1903                var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax";
1904                document.cookie = "mptbm_distance=" + encodeURIComponent(distance) + cookieOptions;
1905                document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions;
1906                document.cookie = "mptbm_duration=" + encodeURIComponent(duration) + cookieOptions;
1907                document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions;
1908
1909                var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area');
1910                if (mapArea.length > 0) {
1911                    if (mapArea.find('input[name="mptbm_hidden_distance"]').length === 0) {
1912                        mapArea.append('<input type="hidden" name="mptbm_hidden_distance" value="" />');
1913                    }
1914                    if (mapArea.find('input[name="mptbm_hidden_duration"]').length === 0) {
1915                        mapArea.append('<input type="hidden" name="mptbm_hidden_duration" value="" />');
1916                    }
1917
1918                    var explicitDistance = document.getElementById('mptbm_calculated_distance');
1919                    if (explicitDistance) {
1920                        explicitDistance.value = distance;
1921                    }
1922                    var explicitDuration = document.getElementById('mptbm_calculated_duration');
1923                    if (explicitDuration) {
1924                        explicitDuration.value = duration;
1925                    }
1926
1927                    if (mapArea.find('input[name="mptbm_hidden_distance_text"]').length === 0) {
1928                        mapArea.append('<input type="hidden" name="mptbm_hidden_distance_text" value="" />');
1929                    }
1930                    if (mapArea.find('input[name="mptbm_hidden_duration_text"]').length === 0) {
1931                        mapArea.append('<input type="hidden" name="mptbm_hidden_duration_text" value="" />');
1932                    }
1933
1934                    mapArea.find('input[name="mptbm_hidden_distance"]').val(distance);
1935                    mapArea.find('input[name="mptbm_hidden_duration"]').val(duration);
1936                    mapArea.find('input[name="mptbm_hidden_distance_text"]').val(distance_text);
1937                    mapArea.find('input[name="mptbm_hidden_duration_text"]').val(duration_text);
1938                }
1939
1940                directionsRenderer.setDirections(result);
1941                if (useShortestRoute) {
1942                    directionsRenderer.setRouteIndex(routeIndex);
1943                }
1944
1945                var currentMapWrap = mptbm_get_current_map_wrap();
1946                jQuery(currentMapWrap).find('.mptbm_total_distance').html(distance_text);
1947                jQuery(currentMapWrap).find('.mptbm_total_time').html(duration_text);
1948                jQuery(currentMapWrap).find('.mptbm_distance_time').slideDown('fast');
1949                mptbm_update_fixed_hours_warning();
1950
1951                // Fit map to show the entire route
1952                var bounds = new google.maps.LatLngBounds();
1953                result.routes[0].legs.forEach(function (leg) {
1954                    bounds.extend(leg.start_location);
1955                    bounds.extend(leg.end_location);
1956                });
1957                mptbm_map.fitBounds(bounds);
1958            } catch (error) {
1959                console.error('[Google Maps Route] Error:', error);
1960            }
1961        } else {
1962            console.error('[Google Maps Route] Status:', status);
1963        }
1964    });
1965}
1966
1967/**
1968 * Replace the displayed distance/time with the server's own measurement.
1969 *
1970 * Used by the OpenStreetMap path, where the browser routes via OSRM but the fare is
1971 * measured server-side through whichever routing service is configured. Silently
1972 * does nothing on failure, leaving the browser's figures in place - a route bar is
1973 * not worth an error message, and the search itself re-measures server-side anyway.
1974 */
1975function mptbm_sync_distance_from_server(startLatLng, endLatLng) {
1976    if (typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined' || !mptbm_ajax.search_nonce) {
1977        return;
1978    }
1979    jQuery.ajax({
1980        type: 'POST',
1981        url: mp_ajax_url,
1982        data: {
1983            action: 'get_mptbm_route_distance',
1984            nonce: mptbm_ajax.search_nonce,
1985            start_place_coordinates: JSON.stringify({ latitude: startLatLng.lat, longitude: startLatLng.lng }),
1986            end_place_coordinates: JSON.stringify({ latitude: endLatLng.lat, longitude: endLatLng.lng })
1987        },
1988        success: function (response) {
1989            if (!response || !response.success || !response.data || !response.data.distance) {
1990                return;
1991            }
1992            var d = response.data;
1993            var currentMapWrap = mptbm_get_current_map_wrap();
1994            var currentSearchArea = jQuery(currentMapWrap).closest('.mptbm_transport_search_area');
1995            jQuery(currentMapWrap).find('.mptbm_total_distance').text(d.distance_text);
1996            jQuery(currentMapWrap).find('.mptbm_total_time').text(d.duration_text);
1997
1998            // Keep every carrier of the distance in step with what is now on screen -
1999            // the cookies the summary panel reads, the hidden fields the search posts,
2000            // and the explicit inputs the add-to-cart flow prefers. Leaving any of them
2001            // holding OSRM's figure would just reintroduce the mismatch one layer down.
2002            var expires = new Date();
2003            expires.setTime(expires.getTime() + 12 * 3600 * 1000);
2004            var cookieOptions = '; expires=' + expires.toUTCString() + '; path=/; SameSite=Lax';
2005            document.cookie = 'mptbm_distance=' + encodeURIComponent(d.distance) + cookieOptions;
2006            document.cookie = 'mptbm_distance_text=' + encodeURIComponent(d.distance_text) + cookieOptions;
2007            document.cookie = 'mptbm_duration=' + encodeURIComponent(d.duration) + cookieOptions;
2008            document.cookie = 'mptbm_duration_text=' + encodeURIComponent(d.duration_text) + cookieOptions;
2009
2010            var mapArea = currentSearchArea;
2011            mapArea.find('input[name="mptbm_hidden_distance"]').val(d.distance);
2012            mapArea.find('input[name="mptbm_hidden_duration"]').val(d.duration);
2013            mapArea.find('input[name="mptbm_hidden_distance_text"]').val(d.distance_text);
2014            mapArea.find('input[name="mptbm_hidden_duration_text"]').val(d.duration_text);
2015
2016            var explicitDistance = document.getElementById('mptbm_calculated_distance');
2017            if (explicitDistance) {
2018                explicitDistance.value = d.distance;
2019            }
2020            var explicitDuration = document.getElementById('mptbm_calculated_duration');
2021            if (explicitDuration) {
2022                explicitDuration.value = d.duration;
2023            }
2024        }
2025    });
2026}
2027
2028function mptbm_init_google_map() {
2029
2030    // Check if Google Maps API is loaded
2031    if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
2032        console.warn("[Google Map] Google Maps API not loaded. Skipping map initialization.");
2033        return false;
2034    }
2035
2036
2037    mptbm_set_cookie_distance_duration();
2038
2039    // Initialize Google Places autocomplete for pickup location
2040    if (jQuery("#mptbm_map_start_place").length > 0) {
2041        var start_place = document.getElementById("mptbm_map_start_place");
2042        var start_place_autoload = new google.maps.places.Autocomplete(start_place);
2043        var mptbm_restrict_search_to_country = jQuery('[name="mptbm_restrict_search_country"]').val();
2044        var mptbm_country = jQuery('[name="mptbm_country"]').val();
2045
2046        if (mptbm_restrict_search_to_country == 'yes') {
2047            start_place_autoload.setComponentRestrictions({
2048                country: [mptbm_country]
2049            });
2050        }
2051
2052        google.maps.event.addListener(start_place_autoload, "place_changed", function () {
2053            var end_place = document.getElementById("mptbm_map_end_place");
2054            var price_based = jQuery('[name="mptbm_price_based"]').val();
2055
2056            // Disable auto-syncing dropoff with pickup unless dropoff is hidden
2057            if (end_place && end_place.type === 'hidden') {
2058                end_place.value = start_place.value;
2059            }
2060
2061            // For fixed_zone_dropoff, set start marker when pickup is searched
2062            if (price_based === 'fixed_zone_dropoff' && start_place_autoload.getPlace()) {
2063                var place = start_place_autoload.getPlace();
2064                if (place.geometry && place.geometry.location) {
2065                    if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) {
2066                        mptbm_start_marker.setMap(null);
2067                    }
2068                    mptbm_start_marker = new google.maps.Marker({
2069                        position: place.geometry.location,
2070                        map: mptbm_map,
2071                        title: place.name || place.formatted_address
2072                    });
2073                    mptbm_map.setCenter(place.geometry.location);
2074                    mptbm_map.setZoom(14);
2075
2076                    // Calculate route if end marker exists
2077                    if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) {
2078                        mptbm_calculate_google_route_from_markers();
2079                    }
2080                }
2081            }
2082
2083            // Focus on Extra Stop if it exists, otherwise Dropoff
2084            var extra_stop = document.getElementById("mptbm_map_extra_stop_place");
2085            if (extra_stop && extra_stop.offsetParent !== null) { // Check if visible
2086                extra_stop.focus();
2087            } else if (end_place && end_place.offsetParent !== null) {
2088                end_place.focus();
2089            }
2090
2091            mptbm_set_cookie_distance_duration(
2092                start_place.value,
2093                end_place ? end_place.value : start_place.value
2094            );
2095        });
2096
2097        // Mark as initialized to prevent duplicate initialization
2098        start_place.setAttribute('data-autocomplete-initialized', 'true');
2099    }
2100
2101    // Ensure Next button is properly positioned after map initialization
2102    setTimeout(function () {
2103        var nextButtonContainer = document.querySelector('.get_details_next_link');
2104        if (nextButtonContainer) {
2105            // Force a reflow to ensure proper positioning
2106            nextButtonContainer.style.display = 'none';
2107            nextButtonContainer.offsetHeight; // Force reflow
2108            nextButtonContainer.style.display = '';
2109
2110            // Ensure it's positioned correctly relative to the map
2111            var mapArea = document.querySelector('.mptbm_map_area');
2112            if (mapArea && mapArea.style.display !== 'none') {
2113                nextButtonContainer.style.marginTop = '20px';
2114                nextButtonContainer.style.position = 'relative';
2115                nextButtonContainer.style.clear = 'both';
2116            }
2117        }
2118    }, 100);
2119
2120    // Initialize Google Places autocomplete for dropoff location (only if it exists and is visible)
2121    if (jQuery("#mptbm_map_end_place").length > 0 && jQuery("#mptbm_map_end_place").is(":visible")) {
2122        var end_place = document.getElementById("mptbm_map_end_place");
2123        var end_place_autoload = new google.maps.places.Autocomplete(end_place);
2124        var mptbm_restrict_search_to_country = jQuery('[name="mptbm_restrict_search_country"]').val();
2125        var mptbm_country = jQuery('[name="mptbm_country"]').val();
2126
2127        if (mptbm_restrict_search_to_country == 'yes') {
2128            end_place_autoload.setComponentRestrictions({
2129                country: [mptbm_country]
2130            });
2131        }
2132
2133        google.maps.event.addListener(end_place_autoload, "place_changed", function () {
2134            var start_place = document.getElementById("mptbm_map_start_place");
2135            var price_based = jQuery('[name="mptbm_price_based"]').val();
2136
2137            // For fixed_zone, set end marker when dropoff is searched
2138            if (price_based === 'fixed_zone' && end_place_autoload.getPlace()) {
2139                var place = end_place_autoload.getPlace();
2140                if (place.geometry && place.geometry.location) {
2141                    if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) {
2142                        mptbm_end_marker.setMap(null);
2143                    }
2144                    mptbm_end_marker = new google.maps.Marker({
2145                        position: place.geometry.location,
2146                        map: mptbm_map,
2147                        title: place.name || place.formatted_address
2148                    });
2149                    mptbm_map.setCenter(place.geometry.location);
2150                    mptbm_map.setZoom(14);
2151
2152                    // Calculate route if start marker exists
2153                    if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) {
2154                        mptbm_calculate_google_route_from_markers();
2155                    }
2156                }
2157            }
2158
2159            mptbm_set_cookie_distance_duration(
2160                start_place ? start_place.value : '',
2161                end_place ? end_place.value : ''
2162            );
2163        });
2164    }
2165
2166    mptbm_render_manual_google_locations();
2167}
2168(function ($) {
2169    "use strict";
2170    $(document).ready(function () {
2171        $(".mpStyle ul.mp_input_select_list").hide();
2172
2173        // Function to initialize Google Places autocomplete (global scope)
2174        window.initializeGooglePlacesAutocomplete = function (retryCount = 0) {
2175            // Check if OpenStreetMap is being used instead
2176            var mapType = document.getElementById('mptbm_map_type');
2177
2178
2179            if (mapType && mapType.value === 'openstreetmap') {
2180                return;
2181            }
2182
2183
2184            // Maximum retry attempts to prevent infinite loops
2185            const MAX_RETRIES = 10;
2186            const INITIAL_DELAY = 100; // Start with 100ms instead of 500ms
2187
2188            // Check if Google Maps API is loaded
2189            if (typeof google === 'undefined' || typeof google.maps === 'undefined' || typeof google.maps.places === 'undefined') {
2190                if (retryCount >= MAX_RETRIES) {
2191                    console.warn('Google Maps API failed to load after', MAX_RETRIES, 'attempts. Please check your API key and connection.');
2192                    return;
2193                }
2194
2195                // Exponential backoff: 100ms, 200ms, 400ms, 800ms, etc.
2196                const delay = INITIAL_DELAY * Math.pow(2, retryCount);
2197
2198                setTimeout(function () {
2199                    initializeGooglePlacesAutocomplete(retryCount + 1);
2200                }, delay);
2201                return;
2202            }
2203
2204            var startPlaceInput = document.getElementById('mptbm_map_start_place');
2205            if (startPlaceInput && !startPlaceInput.hasAttribute('data-autocomplete-initialized')) {
2206                var startPlaceAutocomplete = new google.maps.places.Autocomplete(startPlaceInput);
2207                var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val();
2208                var mptbm_country = $('[name="mptbm_country"]').val();
2209
2210                if (mptbm_restrict_search_to_country == 'yes') {
2211                    startPlaceAutocomplete.setComponentRestrictions({
2212                        country: [mptbm_country]
2213                    });
2214                }
2215
2216                google.maps.event.addListener(startPlaceAutocomplete, "place_changed", function () {
2217                    var endPlaceInput = document.getElementById('mptbm_map_end_place');
2218                    var price_based = $('[name="mptbm_price_based"]').val();
2219                    var end_val = endPlaceInput ? endPlaceInput.value : (startPlaceInput ? startPlaceInput.value : '');
2220
2221                    if (price_based === 'fixed_zone_dropoff' && window.mptbm_fixed_zone_end_coords) {
2222                        end_val = window.mptbm_fixed_zone_end_coords.latitude + "," + window.mptbm_fixed_zone_end_coords.longitude;
2223                    }
2224
2225                    // Only sync dropoff with pickup if dropoff is hidden (hourly pricing with disabled dropoff)
2226                    if (endPlaceInput && endPlaceInput.type === 'hidden') {
2227                        endPlaceInput.value = startPlaceInput.value;
2228                        end_val = startPlaceInput.value;
2229                    }
2230
2231
2232
2233                    mptbm_set_cookie_distance_duration(
2234                        startPlaceInput.value,
2235                        end_val
2236                    );
2237                });
2238
2239                // Mark as initialized to prevent duplicate initialization
2240                startPlaceInput.setAttribute('data-autocomplete-initialized', 'true');
2241            }
2242
2243            // Initialize Google Places autocomplete for dropoff location (only if it exists and is visible)
2244            var endPlaceInput = document.getElementById('mptbm_map_end_place');
2245
2246
2247
2248
2249            if (endPlaceInput && !endPlaceInput.hasAttribute('data-autocomplete-initialized') && endPlaceInput.type !== 'hidden') {
2250                var endPlaceAutocomplete = new google.maps.places.Autocomplete(endPlaceInput);
2251                var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val();
2252                var mptbm_country = $('[name="mptbm_country"]').val();
2253
2254                if (mptbm_restrict_search_to_country == 'yes') {
2255                    endPlaceAutocomplete.setComponentRestrictions({
2256                        country: [mptbm_country]
2257                    });
2258                }
2259
2260                google.maps.event.addListener(endPlaceAutocomplete, 'place_changed', function () {
2261                    var startInput = document.getElementById('mptbm_map_start_place');
2262                    var price_based = $('[name="mptbm_price_based"]').val();
2263                    var start_val = startInput ? startInput.value : '';
2264
2265                    if (price_based === 'fixed_zone' && window.mptbm_fixed_zone_start_coords) {
2266                        start_val = window.mptbm_fixed_zone_start_coords.latitude + "," + window.mptbm_fixed_zone_start_coords.longitude;
2267                    }
2268
2269
2270
2271                    mptbm_set_cookie_distance_duration(
2272                        start_val,
2273                        endPlaceInput ? endPlaceInput.value : ''
2274                    );
2275                });
2276
2277                endPlaceInput.setAttribute('data-autocomplete-initialized', 'true');
2278            }
2279
2280            // Initialize Google Places autocomplete for EXTRA STOP location
2281            var extraStopInput = document.getElementById('mptbm_map_extra_stop_place');
2282
2283            if (extraStopInput && !extraStopInput.hasAttribute('data-autocomplete-initialized')) {
2284                var extraStopAutocomplete = new google.maps.places.Autocomplete(extraStopInput);
2285                var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val();
2286                var mptbm_country = $('[name="mptbm_country"]').val();
2287
2288                if (mptbm_restrict_search_to_country == 'yes') {
2289                    extraStopAutocomplete.setComponentRestrictions({
2290                        country: [mptbm_country]
2291                    });
2292                }
2293
2294                google.maps.event.addListener(extraStopAutocomplete, 'place_changed', function () {
2295                    var place = extraStopAutocomplete.getPlace();
2296
2297                    // Create marker for extra stop
2298                    if (place.geometry && place.geometry.location) {
2299                        // Remove existing extra marker if present
2300                        if (typeof mptbm_extra_marker !== 'undefined' && mptbm_extra_marker) {
2301                            mptbm_extra_marker.setMap(null);
2302                        }
2303
2304                        // Create new marker for extra stop
2305                        mptbm_extra_marker = new google.maps.Marker({
2306                            position: place.geometry.location,
2307                            map: mptbm_map,
2308                            title: place.name || place.formatted_address,
2309                            label: {
2310                                text: 'E',
2311                                color: 'white',
2312                                fontWeight: 'bold'
2313                            }
2314                        });
2315
2316                        mptbm_map.setCenter(place.geometry.location);
2317                        mptbm_map.setZoom(14);
2318                    }
2319
2320                    // Update the map route when extra stop changes
2321                    var startInput = document.getElementById('mptbm_map_start_place');
2322                    var endInput = document.getElementById('mptbm_map_end_place');
2323
2324                    mptbm_set_cookie_distance_duration(
2325                        startInput ? startInput.value : '',
2326                        endInput ? endInput.value : ''
2327                    );
2328                });
2329
2330                extraStopInput.setAttribute('data-autocomplete-initialized', 'true');
2331            }
2332        };
2333
2334        // Maximum retry attempts to prevent infinite loops
2335
2336        // Initialize Google Places autocomplete on page load with a delay to ensure API is loaded
2337        setTimeout(function () {
2338            var mapType = document.getElementById('mptbm_map_type');
2339
2340            if (mapType && mapType.value === 'openstreetmap') {
2341                mptbm_init_osm_address_search();
2342            } else {
2343                initializeGooglePlacesAutocomplete();
2344            }
2345        }, 100); // Reduced from 500ms to 100ms for faster initialization
2346
2347        // Handle Previous/Next button positioning after tab changes
2348        $(document).on('click', '.nextTab_prev, .nextTab_next', function () {
2349            setTimeout(function () {
2350                var nextButtonContainer = document.querySelector('.get_details_next_link');
2351                if (nextButtonContainer) {
2352                    // Force a reflow to ensure proper positioning
2353                    nextButtonContainer.style.display = 'none';
2354                    nextButtonContainer.offsetHeight; // Force reflow
2355                    nextButtonContainer.style.display = '';
2356
2357                    // Ensure it's positioned correctly relative to the map
2358                    var mapArea = document.querySelector('.mptbm_map_area');
2359                    if (mapArea && mapArea.style.display !== 'none') {
2360                        nextButtonContainer.style.marginTop = '20p
2360x';
2361                        nextButtonContainer.style.position = 'relative';
2362                        nextButtonContainer.style.clear = 'both';
2363                    }
2364                }
2365            }, 350); // Wait for slideDown animation to complete
2366        });
2367
2368        // Function to validate and fix tab structure (silent version)
2369        function validateTabStructure() {
2370            // Check tab links
2371            $('.mptb-tabs li').each(function () {
2372                var tabId = $(this).attr('mptbm-data-tab');
2373                var isCurrent = $(this).hasClass('current');
2374
2375                // Check if corresponding tab content exists
2376                var tabContent = $("#" + tabId);
2377                if (tabContent.length === 0) {
2378                    // Create missing tab content container
2379                    var tabContainerParent = $('.mptb-tab-container');
2380                    if (tabContainerParent.length > 0) {
2381                        var newTabContainer = $('<div id="' + tabId + '" class="mptb-tab-content"></div>');
2382                        tabContainerParent.append(newTabContainer);
2383                    }
2384                }
2385            });
2386
2387            // Check tab content containers
2388            $('.mptb-tab-content').each(function () {
2389                var tabId = $(this).attr('id');
2390                var isCurrent = $(this).hasClass('current');
2391                var isVisible = $(this).is(':visible');
2392
2393                // Ensure current tab is visible
2394                if (isCurrent && !isVisible) {
2395                    $(this).css('display', 'block');
2396                }
2397            });
2398        }
2399
2400        // Function to ensure loading spinner element exists
2401        window.ensureLoadingGifExists = function () {
2402            var loadingGif = $('.mptbm-hide-gif');
2403            var tabContainer = $('.mptb-tab-container');
2404
2405            if (loadingGif.length === 0 && tabContainer.length > 0) {
2406                var loadingSpinnerHtml = '<div class="mptbm-hide-gif mptbm-gif" style="display: none;"><div class="mptbm-spinner"></div></div>';
2407                tabContainer.append(loadingSpinnerHtml);
2408                return true;
2409            } else if (loadingGif.length === 0 && tabContainer.length === 0) {
2410                return false;
2411            }
2412            return true;
2413        };
2414
2415        // Try to create loading GIF element immediately
2416        window.ensureLoadingGifExists();
2417
2418        // Also try after a short delay to ensure DOM is fully ready
2419        setTimeout(function () {
2420            window.ensureLoadingGifExists();
2421            validateTabStructure();
2422        }, 100);
2423
2424        // Only initialize map on page load if the first tab should have a map
2425        if ($("#mptbm_map_area").length > 0) {
2426            var hasTabs = $('.mptb-tabs').length > 0;
2427            if (hasTabs) {
2428                // Check if the current tab should have a map
2429                var currentTab = $('.mptb-tabs li.current').attr('mptbm-data-tab');
2430                var mapEnabled = $('.mptb-tabs li.current').attr('mptbm-data-map');
2431
2432                // Manual/flat-rate tabs may now have their own optional location map.
2433                if (mapEnabled === 'yes' && mptbm_get_current_map_wrap() && mptbm_get_current_map_wrap().style.display !== 'none') {
2434                    mptbm_map_area_init();
2435                }
2436            } else {
2437                // No tabs (plain [mptbm_booking]) → initialize map if container is visible
2438                var mapAreaEl = document.querySelector('.mptbm_map_area');
2439                if (!mapAreaEl || mapAreaEl.style.display === 'none') {
2440                    // Skip if hidden by template conditions
2441                } else {
2442                    mptbm_map_area_init();
2443                }
2444            }
2445        }
2446    });
2447
2448    function mptbmNormalizeTimeToken(time) {
2449        var parts = String(time || '').replace(':', '.').split('.');
2450        if (parts.length !== 2) {
2451            return '';
2452        }
2453
2454        var hours = parseInt(parts[0], 10);
2455        var minutes = parseInt(parts[1], 10);
2456        if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
2457            return '';
2458        }
2459
2460        return String(hours).padStart(2, '0') + '.' + String(minutes).padStart(2, '0');
2461    }
2462
2463    function mptbmRefreshVehicleTimeAvailability(parent) {
2464        var vehicleId = parseInt(parent.find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0;
2465        var selectedDate = parent.find('#mptbm_map_start_date').val();
2466        var timeItems = parent.find('.start_time_list li[data-time]');
2467
2468        if (!selectedDate || !timeItems.length || typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined') {
2469            return;
2470        }
2471
2472        var times = [];
2473        timeItems.each(function () {
2474            var time = mptbmNormalizeTimeToken($(this).attr('data-time'));
2475            if (time) {
2476                times.push(time);
2477            }
2478        });
2479
2480        clearTimeout(parent.data('mptbm-time-availability-timer'));
2481        var previousRequest = parent.data('mptbm-time-availability-request');
2482        if (previousRequest && previousRequest.readyState !== 4) {
2483            previousRequest.abort();
2484        }
2485
2486        var timer = setTimeout(function () {
2487            var requestDate = selectedDate;
2488            var request = $.ajax({
2489                type: 'POST',
2490                url: mp_ajax_url,
2491                data: {
2492                    action: 'get_mptbm_vehicle_time_availability',
2493                    nonce: mptbm_ajax.search_nonce,
2494                    vehicle_id: vehicleId,
2495                    date: requestDate,
2496                    price_based: parent.find('[name="mptbm_price_based"]').val() || 'dynamic',
2497                    times: times
2498                },
2499                success: function (response) {
2500                    if (!response.success || parent.find('#mptbm_map_start_date').val() !== requestDate) {
2501                        return;
2502                    }
2503
2504                    var unavailable = response.data.unavailable_times || [];
2505                    var unavailableLookup = {};
2506                    $.each(unavailable, function (index, time) {
2507                        unavailableLookup[mptbmNormalizeTimeToken(time)] = true;
2508                    });
2509
2510                    var unavailableLabel = response.data.unavailable_label || 'Booked';
2511                    var unavailableTitle = response.data.unavailable_title || unavailableLabel;
2512                    var selectedTimeWasDisabled = false;
2513
2514                    parent.find('.start_time_list li[data-time]').each(function () {
2515                        var item = $(this);
2516                        var isUnavailable = !!unavailableLookup[mptbmNormalizeTimeToken(item.attr('data-time'))];
2517
2518                        item.toggleClass('mptbm-time-unavailable', isUnavailable);
2519                        if (isUnavailable) {
2520                            item.attr({
2521                                'aria-disabled': 'true',
2522                                'data-unavailable-label': unavailableLabel,
2523                                'title': unavailableTitle
2524                            });
2525                            if (String(parent.find('#mptbm_map_start_time').val()) === String(item.attr('data-value'))) {
2526                                selectedTimeWasDisabled = true;
2527                            }
2528                        } else {
2529                            item.removeAttr('aria-disabled data-unavailable-label title');
2530                        }
2531                    });
2532
2533                    if (selectedTimeWasDisabled) {
2534                        parent.find('#mptbm_map_start_time, #mptbm_start_time').val('');
2535                    }
2536                }
2537            });
2538
2539            parent.data('mptbm-time-availability-request', request);
2540        }, 75);
2541
2542        parent.data('mptbm-time-availability-timer', timer);
2543    }
2544
2545    $(document).on('mptbm_time_options_updated', function () {
2546        $('.mptbm_transport_search_area').each(function () {
2547            mptbmRefreshVehicleTimeAvailability($(this));
2548        });
2549    });
2550
2551    setTimeout(function () {
2552        $('.mptbm_transport_search_area').each(function () {
2553            mptbmRefreshVehicleTimeAvailability($(this));
2554        });
2555    }, 250);
2556
2557    $(document).on("click", "#mptbm_get_vehicle", function () {
2558        let searchButton = this;
2559        let parent = $(this).closest(".mptbm_transport_search_area");
2560        let mptbm_enable_return_in_different_date = parent
2561            .find('[name="mptbm_enable_return_in_different_date"]')
2562            .val();
2563
2564        let target = parent.find(".mptbm_inline_search_results");
2565        let target_date = parent.find("#mptbm_map_start_date");
2566        let return_target_date = parent.find("#mptbm_map_return_date");
2567        let target_time = parent.find("#mptbm_map_start_time");
2568        let return_target_time = parent.find("#mptbm_map_return_time");
2569        let start_place;
2570        let end_place;
2571        let price_based = parent.find('[name="mptbm_price_based"]').val();
2572        let two_way_field = parent.find('[name="mptbm_taxi_return"]');
2573        let two_way = two_way_field.length ? two_way_field.val() : '1';
2574        let waiting_time = parent.find('[name="mptbm_waiting_time"]').val();
2575        let fixed_time = parent.find('[name="mptbm_fixed_hours"]').val();
2576        let mptbm_original_price_base = parent.find('[name="mptbm_original_price_base"]').val();
2577        let mptbm_source_vehicle_id = parseInt(parent.find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0;
2578
2579
2580        let mptbm_enable_view_search_result_page = parent
2581            .find('[name="mptbm_enable_view_search_result_page"]')
2582            .val();
2583        if (price_based === "manual") {
2584            start_place = document.getElementById("mptbm_manual_start_place");
2585            end_place = document.getElementById("mptbm_manual_end_place");
2586        } else if (price_based === "fixed_zone") {
2587            start_place = document.getElementById("mptbm_manual_start_place");
2588            end_place = document.getElementById("mptbm_map_end_place");
2589        } else if (price_based === "fixed_zone_dropoff") {
2590            start_place = document.getElementById("mptbm_map_start_place");
2591            end_place = document.getElementById("mptbm_manual_end_place");
2592        } else {
2593            start_place = document.getElementById("mptbm_map_start_place");
2594            end_place = document.getElementById("mptbm_map_end_place");
2595        }
2596        let start_date = target_date.val();
2597        let return_date;
2598        let return_time;
2599        let has_return_fields = return_target_date.length > 0 && return_target_time.length > 0;
2600
2601        if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && price_based != 'fixed_hourly' && has_return_fields) {
2602            return_date = return_target_date.val();
2603            return_time = return_target_time.val();
2604
2605            // Get the actual time from the data-time attribute (consistent with start_time)
2606            let selectedReturnTimeElement = parent.find("#mptbm_map_return_time").closest(".mp_input_select").find("li[data-value='" + return_time + "']");
2607            if (selectedReturnTimeElement.length) {
2608                return_time = selectedReturnTimeElement.attr('data-time');
2609            }
2610
2611        } else {
2612            return_date = start_date;
2613            return_time = 'Not applicable';
2614        }
2615        let start_time = target_time.val();
2616        // Get the actual time from the data-time attribute
2617        let selectedTimeElement = parent.find("#mptbm_map_start_time").closest(".mp_input_select").find("li[data-value='" + start_time + "']");
2618        if (selectedTimeElement.length) {
2619            start_time = selectedTimeElement.attr('data-time');
2620
2621        }
2622
2623
2624
2625        // Helper function to safely get value from input or select
2626        function getElementValue(element) {
2627            if (!element) return '';
2628            if (element.tagName === 'SELECT') {
2629                return element.value || '';
2630            }
2631            return element.value || '';
2632        }
2633
2634        let start_place_value = getElementValue(start_place);
2635        let end_place_value = getElementValue(end_place);
2636
2637        if (!start_date) {
2638            target_date.trigger("click");
2639        } else if (start_time === undefined || start_time === null || start_time === '') {
2640            parent
2641                .find("#mptbm_map_start_time")
2642                .closest(".mp_input_select")
2643                .find("input.formControl")
2644                .trigger("click");
2645        } else if (!return_date) {
2646            if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && has_return_fields) {
2647                return_target_date.trigger("click");
2648            }
2649        } else if (return_time === undefined || return_time === null || return_time === '') {
2650            if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && has_return_fields) {
2651                parent
2652                    .find("#mptbm_map_return_time")
2653                    .closest(".mp_input_select")
2654                    .find("input.formControl")
2655                    .trigger("click");
2656            }
2657        } else if (!start_place_value || (start_place && start_place.tagName === 'SELECT' && start_place.options[start_place.selectedIndex] && start_place.options[start_place.selectedIndex].disabled)) {
2658            if (start_place) start_place.focus();
2659            // Show error message
2660            let startMsg = price_based === 'manual' || price_based === 'fixed_zone' ? 'Please select a pickup location' : 'Please enter a pickup location';
2661            if (start_place) showLocationError(start_place, startMsg);
2662        } else if (!end_place_value || (end_place && end_place.tagName === 'SELECT' && end_place.options[end_place.selectedIndex] && end_place.options[end_place.selectedIndex].disabled)) {
2663            // Check if dropoff is required (not hidden for hourly)
2664            let hideDropoff = parent.find('[name="mptbm_original_price_base"]').val() === 'fixed_hourly' &&
2665                document.getElementById('mptbm_map_end_place').type === 'hidden';
2666            if (!hideDropoff) {
2667                end_place.focus();
2668                // Show error message
2669                let endMsg = price_based === 'manual' ? 'Please select a dropoff location' : 'Please enter a dropoff location';
2670                showLocationError(end_place, endMsg);
2671            }
2672        } else {
2673            // Remove any existing error messages
2674            removeLocationErrors();
2675
2676            var manualMapActive = price_based === 'manual' && parent.find('.mptbm_map_area').attr('data-manual-map') === 'yes';
2677            var manualStartCoordinates = parent.data('mptbm-manual-start-coordinates');
2678            var manualEndCoordinates = parent.data('mptbm-manual-end-coordinates');
2679            if (manualMapActive && (!manualStartCoordinates || !manualEndCoordinates)) {
2680                mptbm_search_loading(parent, true);
2681                var startLabel = $(start_place).find('option:selected').data('label') || $(start_place).find('option:selected').text();
2682                var endLabel = $(end_place).find('option:selected').data('label') || $(end_place).find('option:selected').text();
2683                $.when(
2684                    mptbm_resolve_manual_location(start_place_value, startLabel),
2685                    mptbm_resolve_manual_location(end_place_value, endLabel)
2686                ).done(function (resolvedStart, resolvedEnd) {
2687                    mptbm_search_loading(parent, false);
2688                    if (!resolvedStart || !resolvedEnd) {
2689                        showLocationError(end_place, 'Unable to locate this route on the map. Please try again.');
2690                        return;
2691                    }
2692                    parent.data('mptbm-manual-start-coordinates', resolvedStart);
2693                    parent.data('mptbm-manual-end-coordinates', resolvedEnd);
2694                    mptbm_update_manual_route_map(parent);
2695                    $(searchButton).trigger('click');
2696                });
2697                return;
2698            }
2699
2700            mptbm_search_loading(parent, true);
2701            mptbm_content_refresh(parent);
2702            if (price_based !== "manual") {
2703                let calc_start = start_place_value;
2704                let calc_end = end_place_value;
2705
2706                if (price_based === 'fixed_zone' && window.mptbm_fixed_zone_start_coords) {
2707                    calc_start = window.mptbm_fixed_zone_start_coords.latitude + "," + window.mptbm_fixed_zone_start_coords.longitude;
2708                } else if (price_based === 'fixed_zone_dropoff' && window.mptbm_fixed_zone_end_coords) {
2709                    calc_end = window.mptbm_fixed_zone_end_coords.latitude + "," + window.mptbm_fixed_zone_end_coords.longitude;
2710                }
2711
2712                mptbm_set_cookie_distance_duration(calc_start, calc_end);
2713            }
2714            //let price_based = parent.find('[name="mptbm_price_based"]').val();
2715            function getGeometryLocation(address, callback) {
2716                // Check if using OpenStreetMap
2717                var mapType = document.getElementById('mptbm_map_type');
2718                if (mapType && mapType.value === 'openstreetmap') {
2719                    // Use OpenStreetMap geocoding via our proxy
2720                    var ajaxUrl = mptbm_ajax.ajax_url + '?action=mptbm_osm_search&nonce=' + mptbm_ajax.osm_nonce + '&q=' + encodeURIComponent(address);
2721
2722                    fetch(ajaxUrl)
2723                        .then(response => response.json())
2724                        .then(response => {
2725                            if (response.success && response.data && response.data.length > 0) {
2726                                var result = response.data[0];
2727                                var coordinatesOfPlace = {
2728                                    "latitude": parseFloat(result.lat),
2729                                    "longitude": parseFloat(result.lon)
2730                                };
2731                                callback(coordinatesOfPlace);
2732                            } else {
2733                                console.error("OSM geocoding failed for:", address);
2734                                callback(null);
2735                            }
2736                        })
2737                        .catch(error => {
2738                            console.error("Error in OSM geocoding:", error);
2739                            callback(null);
2740                        });
2741                } else {
2742                    // Use Google Maps geocoding
2743                    var geocoder = new google.maps.Geocoder();
2744                    var coordinatesOfPlace = {};
2745
2746                    geocoder.geocode({ address: address }, function (results, status) {
2747
2748                        if (status === "OK") {
2749                            try {
2750                                var latitude = results[0].geometry.location.lat();
2751                                var longitude = results[0].geometry.location.lng();
2752                                coordinatesOfPlace["latitude"] = latitude;
2753                                coordinatesOfPlace["longitude"] = longitude;
2754                                // Call the callback function with the coordinates
2755                                callback(coordinatesOfPlace);
2756                            } catch (error) {
2757                                console.error("Error processing geocoding results:", error);
2758                                callback(null);
2759                            }
2760                        } else {
2761                            console.error(
2762                                "Geocode was not successful for the following reason: " + status
2763                            );
2764                            // Call the callback function with null to indicate failure
2765                            callback(null);
2766                        }
2767                    });
2768                }
2769            }
2770            // Define a function to get the coordinates asynchronously and return a Deferred object
2771
2772            function getCoordinatesAsync(address) {
2773                var deferred = $.Deferred();
2774                getGeometryLocation(address, function (coordinates) {
2775                    deferred.resolve(coordinates);
2776                });
2777                return deferred.promise();
2778            }
2779
2780            // In OSM mode, picking a suggestion from the pickup/drop-off
2781            // autocomplete already geocoded this exact text once
2782            // (mptbm_handle_osm_address_selection stores the result +
2783            // the matched text). Re-geocoding the same address again here
2784            // was pure waste -- an extra round trip through the slower
2785            // server-side lookup, on every single Search click, for
2786            // something already known. Reuse it when the field's current
2787            // value still matches exactly what was picked; otherwise the
2788            // visitor edited the text since then, so fall back to a fresh
2789            // lookup rather than trust stale coordinates.
2790            function getCachedOrFreshCoordinates(address, cachedCoords, cachedAddress) {
2791                if (cachedCoords && cachedAddress === address) {
2792                    return $.Deferred().resolve(cachedCoords).promise();
2793                }
2794                return getCoordinatesAsync(address);
2795            }
2796            if (price_based !== 'manual') {
2797
2798                // For fixed_zone, pickup is from dropdown (term_XX), so we use pre-stored coords
2799                // Only geocode the end_place (dropoff search input)
2800                // For fixed_zone_dropoff, pickup is from map search, dropoff is from dropdown
2801                if (price_based === 'fixed_zone' || price_based === 'fixed_zone_dropoff') {
2802                    let searchInput, dropdownCoords, startCoordinates, endCoordinates;
2803
2804                    if (price_based === 'fixed_zone') {
2805                        // fixed_zone: pickup = dropdown, dropoff = map search
2806                        searchInput = end_place;
2807                        dropdownCoords = window.mptbm_fixed_zone_start_coords || null;
2808
2809                        let searchInputValue = getElementValue(searchInput);
2810                        getCachedOrFreshCoordinates(searchInputValue, window.mptbm_fixed_zone_end_coords, window.mptbm_osm_end_coords_address).done(function (searchCoordinates) {
2811                            if (!searchCoordinates || searchCoordinates === null) {
2812                                mptbm_search_loading(parent, false);
2813                                showLocationError(end_place, 'Invalid dropoff location. Please select a valid address.');
2814                                end_place.focus();
2815                                return;
2816                            }
2817
2818                            startCoordinates = dropdownCoords;
2819                            endCoordinates = searchCoordinates;
2820
2821     
2821                       submitFixedZoneSearch();
2822                        });
2823                    } else {
2824                        // fixed_zone_dropoff: pickup = map search, dropoff = dropdown
2825                        searchInput = start_place;
2826                        dropdownCoords = window.mptbm_fixed_zone_end_coords || null;
2827
2828                        if (!dropdownCoords) {
2829                            mptbm_search_loading(parent, false);
2830                            showLocationError(end_place, 'Please select a dropoff location from the dropdown.');
2831                            parent.find("#mptbm_manual_end_place").focus();
2832                            return;
2833                        }
2834
2835                        let searchInputValue = getElementValue(searchInput);
2836                        getCachedOrFreshCoordinates(searchInputValue, window.mptbm_fixed_zone_start_coords, window.mptbm_osm_start_coords_address).done(function (searchCoordinates) {
2837                            if (!searchCoordinates || searchCoordinates === null) {
2838                                mptbm_search_loading(parent, false);
2839                                showLocationError(start_place, 'Invalid pickup location. Please select a valid address.');
2840                                start_place.focus();
2841                                return;
2842                            }
2843
2844                            startCoordinates = searchCoordinates;
2845                            endCoordinates = dropdownCoords;
2846
2847                            submitFixedZoneSearch();
2848                        });
2849                    }
2850
2851                    function submitFixedZoneSearch() {
2852                        let start_val = getElementValue(start_place);
2853                        let end_val = getElementValue(end_place);
2854                        if (start_val && end_val && start_date &&
2855                            (start_time !== undefined && start_time !== null && start_time !== '') &&
2856                            return_date &&
2857                            (return_time !== undefined && return_time !== null && return_time !== '')) {
2858                            let actionValue;
2859                            if (!mptbm_enable_view_search_result_page) {
2860                                actionValue = "get_mptbm_map_search_result";
2861                                $.ajax({
2862                                    type: "POST",
2863                                    url: mp_ajax_url,
2864                                    data: {
2865                                        action: actionValue,
2866                                        nonce: mptbm_ajax.search_nonce,
2867                                        mptbm_source_vehicle_id: mptbm_source_vehicle_id,
2868                                        start_place: start_val,
2869                                        start_place_coordinates: JSON.stringify(startCoordinates),
2870                                        end_place_coordinates: JSON.stringify(endCoordinates),
2871                                        end_place: end_val,
2872                                        start_date: start_date,
2873                                        start_time: start_time,
2874                                        price_based: price_based,
2875                                        two_way: two_way,
2876                                        waiting_time: waiting_time,
2877                                        fixed_time: fixed_time,
2878                                        return_date: return_date,
2879                                        return_time: return_time,
2880                                        mptbm_passengers: parent.find('#mptbm_passengers').val(),
2881                                        mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
2882                                        mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
2883                                        mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
2884                                        mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
2885                                        mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
2886                                        mptbm_original_price_base: mptbm_original_price_base,
2887                                        mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
2888                                        mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
2889                                    },
2890                                    success: function (data) {
2891                                        if (data.success === false) {
2892                                            alert(data.data.message || 'An error occurred. Please try again.');
2893                                            mptbm_search_loading(parent, false);
2894                                            return;
2895                                        }
2896                                        target.append(data).promise().done(function () {
2897                                            mptbm_search_loading(parent, false);
2898                                            mptbm_reveal_inline_results(target);
2899                                            if (mptbm_is_ios()) {
2900                                                target[0].style.display = 'none';
2901                                                void target[0].offsetHeight;
2902                                                target[0].style.display = '';
2903                                            }
2904                                        });
2905                                    },
2906                                    error: function (response) {
2907                                        console.log(response);
2908                                    },
2909                                });
2910                            } else {
2911                                actionValue = "get_mptbm_map_search_result_redirect";
2912                                $.ajax({
2913                                    type: "POST",
2914                                    url: mp_ajax_url,
2915                                    data: {
2916                                        action: actionValue,
2917                                        nonce: mptbm_ajax.search_nonce,
2918                                        mptbm_source_vehicle_id: mptbm_source_vehicle_id,
2919                                        start_place: start_val,
2920                                        start_place_coordinates: JSON.stringify(startCoordinates),
2921                                        end_place_coordinates: JSON.stringify(endCoordinates),
2922                                        end_place: end_val,
2923                                        start_date: start_date,
2924                                        start_time: start_time,
2925                                        price_based: price_based,
2926                                        two_way: two_way,
2927                                        waiting_time: waiting_time,
2928                                        fixed_time: fixed_time,
2929                                        return_date: return_date,
2930                                        return_time: return_time,
2931                                        mptbm_passengers: parent.find('#mptbm_passengers').val(),
2932                                        mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
2933                                        mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
2934                                        mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
2935                                        mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
2936                                        mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
2937                                        mptbm_original_price_base: mptbm_original_price_base,
2938                                        mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
2939                                        mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
2940                                    },
2941                                    success: function (data) {
2942                                        if (data.success === false) {
2943                                            alert(data.data.message || 'An error occurred. Please try again.');
2944                                            mptbm_search_loading(parent, false);
2945                                            return;
2946                                        }
2947
2948                                        var redirectUrl = mptbm_resolve_redirect_url(data);
2949                                        if (!redirectUrl) {
2950                                            mptbm_search_loading(parent, false);
2951                                            alert('Unable to open the search results page. Please try again.');
2952                                            return;
2953                                        }
2954
2955                                        window.location.href = redirectUrl;
2956                                    },
2957                                    error: function (response) {
2958                                        console.log(response);
2959                                    },
2960                                });
2961                            }
2962                        }
2963                    }
2964
2965                    return; // Exit early for fixed_zone
2966                }
2967
2968                $.when(
2969                    getCachedOrFreshCoordinates(start_place.value, window.mptbm_fixed_zone_start_coords, window.mptbm_osm_start_coords_address),
2970                    getCachedOrFreshCoordinates(end_place.value, window.mptbm_fixed_zone_end_coords, window.mptbm_osm_end_coords_address)
2971                ).done(function (startCoordinates, endCoordinates) {
2972                    // Validate that geocoding was successful
2973                    if (!startCoordinates || startCoordinates === null) {
2974                        mptbm_search_loading(parent, false);
2975                        showLocationError(start_place, 'Invalid pickup location. Please select a valid address.');
2976                        start_place.focus();
2977                        return;
2978                    }
2979
2980                    if (!endCoordinates || endCoordinates === null) {
2981                        mptbm_search_loading(parent, false);
2982                        showLocationError(end_place, 'Invalid dropoff location. Please select a valid address.');
2983                        end_place.focus();
2984                        return;
2985                    }
2986
2987                    if (start_place.value && end_place.value && start_date &&
2988                        (start_time !== undefined && start_time !== null && start_time !== '') &&
2989                        return_date &&
2990                        (return_time !== undefined && return_time !== null && return_time !== '')) {
2991                        let actionValue;
2992                        if (!mptbm_enable_view_search_result_page) {
2993                            actionValue = "get_mptbm_map_search_result";
2994                            $.ajax({
2995                                type: "POST",
2996                                url: mp_ajax_url,
2997                                data: {
2998                                    action: actionValue,
2999                                    nonce: mptbm_ajax.search_nonce,
3000                                    mptbm_source_vehicle_id: mptbm_source_vehicle_id,
3001                                    start_place: start_place.value,
3002                                    start_place_coordinates: startCoordinates,
3003                                    end_place_coordinates: endCoordinates,
3004                                    end_place: end_place.value,
3005                                    start_date: start_date,
3006                                    start_time: start_time,
3007                                    price_based: price_based,
3008                                    two_way: two_way,
3009                                    waiting_time: waiting_time,
3010                                    fixed_time: fixed_time,
3011                                    return_date: return_date,
3012                                    return_time: return_time,
3013                                    mptbm_passengers: parent.find('#mptbm_passengers').val(),
3014                                    mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
3015                                    mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
3016                                    mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
3017                                    mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
3018                                    mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
3019                                    mptbm_original_price_base: mptbm_original_price_base,
3020                                    mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
3021                                    mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
3022                                },
3023                                beforeSend: function () {
3024                                    //mptbm_search_loading(parent, true);
3025                                },
3026                                success: function (data) {
3027                                    // Check if the response is an error
3028                                    if (data.success === false) {
3029                                        alert(data.data.message || 'An error occurred. Please try again.');
3030                                        mptbm_search_loading(parent, false);
3031                                        return;
3032                                    }
3033
3034                                    target
3035                                        .append(data)
3036                                        .promise()
3037                                        .done(function () {
3038                                            mptbm_search_loading(parent, false);
3039                                            mptbm_reveal_inline_results(target);
3040                                            // iOS DOM reflow workaround
3041                                            if (mptbm_is_ios()) {
3042                                                target[0].style.display = 'none';
3043                                                void target[0].offsetHeight;
3044                                                target[0].style.display = '';
3045                                            }
3046                                        });
3047                                },
3048                                error: function (response) {
3049                                    console.log(response);
3050                                },
3051                            });
3052                        } else {
3053                            actionValue = "get_mptbm_map_search_result_redirect";
3054                            $.ajax({
3055                                type: "POST",
3056                                url: mp_ajax_url,
3057                                data: {
3058                                    action: actionValue,
3059                                    nonce: mptbm_ajax.search_nonce,
3060                                    mptbm_source_vehicle_id: mptbm_source_vehicle_id,
3061                                    start_place: start_place.value,
3062                                    start_place_coordinates: startCoordinates,
3063                                    end_place_coordinates: endCoordinates,
3064                                    end_place: end_place.value,
3065                                    start_date: start_date,
3066                                    start_time: start_time,
3067                                    price_based: price_based,
3068                                    two_way: two_way,
3069                                    waiting_time: waiting_time,
3070                                    
3070fixed_time: fixed_time,
3071                                    return_date: return_date,
3072                                    return_time: return_time,
3073                                    mptbm_enable_view_search_result_page: mptbm_enable_view_search_result_page,
3074                                    mptbm_passengers: parent.find('#mptbm_passengers').val(),
3075                                    mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
3076                                    mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
3077                                    mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
3078                                    mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
3079                                    mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
3080                                    mptbm_original_price_base: mptbm_original_price_base,
3081                                    mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
3082                                    mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
3083                                },
3084                                beforeSend: function () {
3085                                    mptbm_search_loading(parent, true);
3086                                },
3087                                success: function (data) {
3088                                    // Check if the response is an error
3089                                    if (data.success === false) {
3090                                        alert(data.data.message || 'An error occurred. Please try again.');
3091                                        mptbm_search_loading(parent, false);
3092                                        return;
3093                                    }
3094
3095                                    var redirectUrl = mptbm_resolve_redirect_url(data);
3096                                    if (!redirectUrl) {
3097                                        mptbm_search_loading(parent, false);
3098                                        alert('Unable to open the search results page. Please try again.');
3099                                        return;
3100                                    }
3101
3102                                    window.location.href = redirectUrl;
3103                                },
3104                                error: function (response) {
3105                                    console.log(response);
3106                                },
3107                            });
3108                        }
3109                    }
3110                });
3111            } else {
3112
3113                if (start_place.value && end_place.value && start_date &&
3114                    (start_time !== undefined && start_time !== null && start_time !== '') &&
3115                    return_date &&
3116                    (return_time !== undefined && return_time !== null && return_time !== '')) {
3117
3118                    let actionValue;
3119                    if (!mptbm_enable_view_search_result_page) {
3120                        actionValue = "get_mptbm_map_search_result";
3121                        $.ajax({
3122                            type: "POST",
3123                            url: mp_ajax_url,
3124                            data: {
3125                                action: actionValue,
3126                                nonce: mptbm_ajax.search_nonce,
3127                                mptbm_source_vehicle_id: mptbm_source_vehicle_id,
3128                                start_place: start_place.value,
3129                                start_place_coordinates: manualStartCoordinates ? JSON.stringify(manualStartCoordinates) : '',
3130                                end_place: end_place.value,
3131                                end_place_coordinates: manualEndCoordinates ? JSON.stringify(manualEndCoordinates) : '',
3132                                start_date: start_date,
3133                                start_time: start_time,
3134                                price_based: price_based,
3135                                two_way: two_way,
3136                                waiting_time: waiting_time,
3137                                fixed_time: fixed_time,
3138                                return_date: return_date,
3139                                return_time: return_time,
3140                                mptbm_passengers: parent.find('#mptbm_passengers').val(),
3141                                mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
3142                                mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
3143                                mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
3144                                mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
3145                                mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
3146                                mptbm_original_price_base: mptbm_original_price_base,
3147                                mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
3148                                mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
3149                                mptbm_distance_text: parent.find('input[name="mptbm_hidden_distance_text"]').val(),
3150                                mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(),
3151                            },
3152                            beforeSend: function () {
3153                                //mptbm_search_loading(parent, true);
3154                            },
3155                            success: function (data) {
3156                                // Check if the response is an error
3157                                if (data.success === false) {
3158                                    alert(data.data.message || 'An error occurred. Please try again.');
3159                                    mptbm_search_loading(parent, false);
3160                                    return;
3161                                }
3162
3163                                target
3164                                    .append(data)
3165                                    .promise()
3166                                    .done(function () {
3167                                        mptbm_search_loading(parent, false);
3168                                        mptbm_reveal_inline_results(target);
3169                                        // iOS DOM reflow workaround
3170                                        if (mptbm_is_ios()) {
3171                                            target[0].style.display = 'none';
3172                                            void target[0].offsetHeight;
3173                                            target[0].style.display = '';
3174                                        }
3175                                    });
3176                            },
3177                            error: function (response) {
3178                                console.log(response);
3179                            },
3180                        });
3181                    } else {
3182                        actionValue = "get_mptbm_map_search_result_redirect";
3183                        $.ajax({
3184                            type: "POST",
3185                            url: mp_ajax_url,
3186                            data: {
3187                                action: actionValue,
3188                                nonce: mptbm_ajax.search_nonce,
3189                                mptbm_source_vehicle_id: mptbm_source_vehicle_id,
3190                                start_place: start_place.value,
3191                                start_place_coordinates: manualStartCoordinates ? JSON.stringify(manualStartCoordinates) : '',
3192                                end_place: end_place.value,
3193                                end_place_coordinates: manualEndCoordinates ? JSON.stringify(manualEndCoordinates) : '',
3194                                start_date: start_date,
3195                                start_time: start_time,
3196                                price_based: price_based,
3197                                two_way: two_way,
3198                                waiting_time: waiting_time,
3199                                fixed_time: fixed_time,
3200                                return_date: return_date,
3201                                return_time: return_time,
3202                                mptbm_enable_view_search_result_page: mptbm_enable_view_search_result_page,
3203                                mptbm_passengers: parent.find('#mptbm_passengers').val(),
3204                                mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
3205                                mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
3206                                mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
3207                                mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent),
3208                                mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent),
3209                                mptbm_original_price_base: mptbm_original_price_base,
3210                                mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(),
3211                                mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(),
3212                                mptbm_distance_text: parent.find('input[name="mptbm_hidden_distance_text"]').val(),
3213                                mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(),
3214                            },
3215                            beforeSend: function () {
3216                                mptbm_search_loading(parent, true);
3217                            },
3218                            success: function (data) {
3219                                // Check if the response is an error
3220                                if (data.success === false) {
3221                                    alert(data.data.message || 'An error occurred. Please try again.');
3222                                    mptbm_search_loading(parent, false);
3223                                    return;
3224                                }
3225
3226                                var redirectUrl = mptbm_resolve_redirect_url(data);
3227                                if (!redirectUrl) {
3228                                    mptbm_search_loading(parent, false);
3229                                    alert('Unable to open the search results page. Please try again.');
3230                                    return;
3231                                }
3232
3233                                window.location.href = redirectUrl;
3234                            },
3235                            error: function (response) {
3236                                console.log(response);
3237                            },
3238                        });
3239                    }
3240                }
3241            }
3242        }
3243    });
3244    $(document).on("change", "#mptbm_map_start_date", function (e, meta) {
3245        let parent = $(this).closest(".mptbm_transport_search_area");
3246        // Clear the time slots list
3247        parent.find('#mptbm_map_start_time').siblings('.start_time_list').empty();
3248        parent.find('.start_time_input,#mptbm_map_start_time').val('');
3249        let mptbm_enable_return_in_different_date = $('[name="mptbm_enable_return_in_different_date"]').val();
3250        let mptbm_buffer_end_minutes = parseInt($('[name="mptbm_buffer_end_minutes"]').val()) || 0;
3251        let mptbm_first_calendar_date = $('[name="mptbm_first_calendar_date"]').val();
3252
3253        var selectedDate = $('#mptbm_map_start_date').val();
3254        var formattedDate = flatpickr.parseDate(selectedDate, 'Y-m-d');
3255
3256        // Get today's date in YYYY-MM-DD format
3257        var today = new Date();
3258        var day = String(today.getDate()).padStart(2, '0');
3259        var month = String(today.getMonth() + 1).padStart(2, '0');
3260        var year = today.getFullYear();
3261        var currentDate = year + '-' + month + '-' + day;
3262
3263        if (selectedDate == currentDate) {
3264            // For today's date, apply buffer time restrictions
3265            var currentTime = new Date();
3266            var currentHour = currentTime.getHours();
3267            var currentMinutes = currentTime.getMinutes();
3268            var currentTotalMinutes = (currentHour * 60) + currentMinutes;
3269
3270            $('.start_time_list-no-dsiplay li').each(function () {
3271                const timeValue = parseFloat($(this).attr('data-value'));
3272                const timeInMinutes = Math.floor(timeValue) * 60 + ((timeValue % 1) * 100);
3273
3274                // Only show times that are after the buffer period
3275                if (timeInMinutes > mptbm_buffer_end_minutes) {
3276                    $('#mptbm_map_start_time').siblings('.start_time_list').append($(this).clone());
3277                }
3278            });
3279        } else if (selectedDate == mptbm_first_calendar_date) {
3280
3281            // For the first available date (which might be today or tomorrow depending on buffer)
3282            $('.start_time_list-no-dsiplay li').each(function () {
3283                const timeValue = parseFloat($(this).attr('data-value'));
3284                const timeInMinutes = Math.floor(timeValue) * 60 + ((timeValue % 1) * 100);
3285
3286                // If this is tomorrow and buffer extends to tomorrow, apply buffer
3287                if (mptbm_buffer_end_minutes > 1440) {
3288                    const adjustedBufferMinutes = mptbm_buffer_end_minutes - 1440;
3289                    if (timeInMinutes > adjustedBufferMinutes) {
3290                        $('#mptbm_map_start_time')
3291                            .siblings('.start_time_list')
3292                            .append($(this).clone());
3293                    }
3294
3295                } else if (mptbm_buffer_end_minutes < 1440 && mptbm_buffer_end_minutes > 0) {
3296                    // ✅ If buffer does not extend to tomorrow, show time after buffer end time
3297                    if (timeInMinutes >= mptbm_buffer_end_minutes) {
3298                        $('#mptbm_map_start_time')
3299                            .siblings('.start_time_list')
3300                            .append($(this).clone());
3301                    }
3302
3303                } else {
3304                    // For other dates or no buffer, show all times
3305                    $('#mptbm_map_start_time')
3306                        .siblings('.start_time_list')
3307                        .append($(this).clone());
3308                }
3309            });
3310        }
3311        else {
3312            // For future dates, show all available times
3313            $('.start_time_list-no-dsiplay li').each(function () {
3314                $('#mptbm_map_start_time').siblings('.start_time_list').append($(this).clone());
3315            });
3316        }
3317
3318        // Update the return date picker if needed
3319        if (mptbm_enable_return_in_different_date == 'yes') {
3320            var fpReturn = $('#mptbm_return_date')[0];
3321            if (fpReturn && fpReturn._flatpickr) {
3322                fpReturn._flatpickr.set('minDate', formattedDate);
3323            }
3324        }
3325
3326        mptbmRefreshVehicleTimeAvailability(parent);
3327        mptbm_content_refresh(parent);
3328        // Auto-opening the time dropdown is a "guide the user to the next
3329        // field" convenience for when they've just picked a date themselves -
3330        // this same "change" event also fires once on every tab load/init
3331        // (flatpickr's onReady syncing its defaultDate into this hidden field,
3332        // see MP_Global_Function::date_picker_js) with meta.initial set, which
3333        // should rebuild the time list above but not pop its dropdown open.
3334        if (!(meta && meta.initial)) {
3335            parent
3336                .find("#mptbm_map_start_time")
3337                .closest(".mp_input_select")
3338                .find("input.formControl")
3339                .trigger("click");
3340        }
3341    });
3342
3343
3344    $(document).on("change", "#mptbm_map_return_date", function () {
3345        let mptbm_enable_return_in_different_date = $('[name="mptbm_enable_return_in_different_date"]').val();
3346
3347        if (mptbm_enable_return_in_different_date == 'yes') {
3348            var selectedTime = parseFloat($('#mptbm_map_start_time').val());
3349            var selectedDate = $('#mptbm_map_start_date').val();
3350            var dateValue = $('#mptbm_map_return_date').val();
3351
3352            // Check if the return date is the same as the pickup date
3353            if (selectedDate == dateValue) {
3354                $('#return_time_list').show();
3355                // Clear existing options
3356                $('#mptbm_map_return_time').siblings('.mp_input_select_list').empty();
3357                $('.mptbm_map_return_time_input').val('');
3358                // If return date is the same as the pickup date, show only times after pickup time
3359                $('.mp_input_select_list li').each(function () {
3360                    var timeValue = parseFloat($(this).attr('data-value'));
3361                    if (timeValue > selectedTime) {
3362                        $('#mptbm_map_return_time').siblings('.mp_input_select_list').append($(this).clone());
3363                    }
3364                });
3365            } else {
3366                // Clear existing options
3367                $('#mptbm_map_return_time').siblings('.mp_input_select_list').empty();
3368                $('.mptbm_map_return_time_input').val('');
3369                $('.return_time_list-no-dsiplay li').each(function () {
3370                    var timeValue = parseFloat($(this).attr('data-value'));
3371                    $('#mptbm_map_return_time').siblings('.mp_input_select_list').append($(this).clone());
3372                });
3373            }
3374        }
3375
3376        // Trigger refresh and display logic
3377        let parent = $(this).closest(".mptbm_transport_search_area");
3378        mptbm_content_refresh(parent);
3379        parent.find("#mptbm_map_return_time").closest(".mp_input_select").find("input.formControl").trigger("click");
3380    });
3381
3382
3383    $(document).on("click", ".start_time_list li", function () {
3384        if ($(this).attr('aria-disabled') === 'true') {
3385            return false;
3386        }
3387        let selectedValue = $(this).attr('data-value');
3388        $(this).closest('.mptbm_transport_search_area').find('#mptbm_map_start_time').val(selectedValue).trigger('change');
3389    });
3390    $(document).on("click", ".return_time_list li", function () {
3391        let selectedValue = $(this).attr('data-value');
3392        $('#mptbm_map_return_time').val(selectedValue).trigger('change');
3393    });
3394    $(document).on("change", "#mptbm_map_start_time", function () {
3395        let parent = $(this).closest(".mptbm_transport_search_area");
3396        mptbm_content_refresh(parent);
3397        parent.find("#mptbm_map_start_place").focus();
3398    });
3399    $(document).on("change", "#mptbm_manual_start_place", function () {
3400        let parent = $(this).closest(".mptbm_transport_search_area");
3401        parent.removeData('mptbm-manual-start-coordinates mptbm-manual-end-coordinates');
3402        mptbm_content_refresh(parent);
3403        let start_place = $(this).val();
3404        let target = parent.find(".mptbm_manual_end_place");
3405        let price_based = parent.find('[name="mptbm_price_based"]').val();
3406
3407        // For fixed_zone (pickup), place marker on map using geo coordinates
3408        if (price_based === "fixed_zone" && start_place) {
3409            let selectedOption = $(this).find('option:selected');
3410            let geoCoords = selectedOption.data('geo');
3411            let locationLabel = selectedOption.data('label') || selectedOption.text();
3412
3413
3414
3415            if (geoCoords) {
3416                let coords = geoCoords.split(',');
3417                let lat = parseFloat(coords[0]);
3418                let lng = parseFloat(coords[1]);
3419
3420                if (!isNaN(lat) && !isNaN(lng)) {
3421                    // Check if using OpenStreetMap or Google Maps
3422                    var mapType = document.getElementById('mptbm_map_type');
3423
3424                    if (mapType && mapType.value === 'openstreetmap') {
3425                        // OpenStreetMap (Leaflet)
3426                        if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) {
3427                            // Clear existing markers
3428                            if (typeof mptbm_osm_start_marker !== 'undefined' && mptbm_osm_start_marker) {
3429                                mptbm_osm_map.removeLayer(mptbm_osm_start_marker);
3430                            }
3431                            // Add new marker
3432                            mptbm_osm_start_marker = L.marker([lat, lng]).addTo(mptbm_osm_map);
3433                            mptbm_osm_map.setView([lat, lng], 14);
3434
3435                            // Store coordinates for later use
3436                            window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng };
3437
3438
3439                            // Calculate route if end marker exists
3440                            if (typeof mptbm_osm_end_marker !== 'undefined' && mptbm_osm_end_marker) {
3441                                mptbm_calculate_osm_distance();
3442                            }
3443                        } else {
3444
3445                        }
3446                    } else if (typeof google !== 'undefined' && typeof google.maps !== 'undefined') {
3447                        // Google Maps
3448                        if (typeof mptbm_map !== 'undefined' && mptbm_map) {
3449                            var latLng = new google.maps.LatLng(lat, lng);
3450
3451                            // Clear existing start marker
3452                            if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) {
3453                                mptbm_start_marker.setMap(null);
3454                            }
3455
3456                            // Add new marker
3457                            mptbm_start_marker = new google.maps.Marker({
3458                                position: latLng,
3459                                map: mptbm_map,
3460                                title: locationLabel
3461                            });
3462
3463                            mptbm_map.setCenter(latLng);
3464                            mptbm_map.setZoom(14);
3465
3466                            // Store coordinates for later use
3467                            window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng };
3468
3469
3470                            // Update distance cookie for Google Maps rout
3470ing
3471                            var latLngStr = lat + "," + lng;
3472                            var endPlace = document.getElementById('mptbm_map_end_place');
3473                            mptbm_set_cookie_distance_duration(latLngStr, endPlace ? endPlace.value : '');
3474
3475                            // Calculate route if end marker exists
3476                            if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) {
3477                                mptbm_calculate_google_route_from_markers();
3478                            }
3479                        }
3480                    }
3481                }
3482            }
3483
3484
3485        } else {
3486            // Reset start coordinates if no place selected
3487            window.mptbm_fixed_zone_start_coords = null;
3488        }
3489
3490        if (start_place) {
3491            let end_place = "";
3492            if (price_based === "manual") {
3493                let post_id = parent.find('[name="mptbm_post_id"]').val() || parent.find('[name="mptbm_source_vehicle_id"]').val();
3494                $.ajax({
3495                    type: "POST",
3496                    url: mp_ajax_url,
3497                    data: {
3498                        action: "get_mptbm_end_place",
3499                        nonce: mptbm_ajax.search_nonce,
3500                        start_place: start_place,
3501                        price_based: price_based,
3502                        post_id: post_id,
3503                    },
3504                    beforeSend: function () {
3505                        // Remove any existing custom dropdown before AJAX call
3506                        $('.mptbm-custom-select-wrapper').remove();
3507                        dLoader(target.closest(".mptbm_search_area"));
3508                    },
3509                    success: function (data) {
3510                        target
3511                            .html(data)
3512                            .promise()
3513                            .done(function () {
3514                                dLoaderRemove(target.closest(".mptbm_search_area"));
3515                                // iOS DOM reflow workaround
3516                                if (mptbm_is_ios()) {
3517                                    target[0].style.display = 'none';
3518                                    void target[0].offsetHeight;
3519                                    target[0].style.display = '';
3520                                }
3521
3522                                // Add a small delay to ensure the select is properly updated
3523                                setTimeout(function () {
3524                                    //console.log('Select updated, options count:', target.find('option:not([disabled])').length);
3525                                }, 100);
3526                            });
3527                    },
3528                    error: function (response) {
3529                        console.log('AJAX error for end locations:', response);
3530                    },
3531                });
3532            }
3533        }
3534    });
3535    $(document).on("change", "#mptbm_manual_end_place", function () {
3536        let parent = $(this).closest(".mptbm_transport_search_area");
3537        parent.removeData('mptbm-manual-end-coordinates');
3538        mptbm_content_refresh(parent);
3539        let end_place = $(this).val();
3540        let price_based = parent.find('[name="mptbm_price_based"]').val();
3541
3542        // For fixed_zone_dropoff, place marker on map using geo coordinates
3543        if (price_based === "fixed_zone_dropoff" && end_place) {
3544            let selectedOption = $(this).find('option:selected');
3545            let geoCoords = selectedOption.data('geo');
3546            let locationLabel = selectedOption.data('label') || selectedOption.text();
3547
3548
3549
3550            if (geoCoords) {
3551                let coords = geoCoords.split(',');
3552                let lat = parseFloat(coords[0]);
3553                let lng = parseFloat(coords[1]);
3554
3555                if (!isNaN(lat) && !isNaN(lng)) {
3556                    var mapType = document.getElementById('mptbm_map_type');
3557
3558                    if (mapType && mapType.value === 'openstreetmap') {
3559                        if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) {
3560                            if (typeof mptbm_osm_end_marker !== 'undefined' && mptbm_osm_end_marker) {
3561                                mptbm_osm_map.removeLayer(mptbm_osm_end_marker);
3562                            }
3563                            mptbm_osm_end_marker = L.marker([lat, lng]).addTo(mptbm_osm_map);
3564                            mptbm_osm_map.setView([lat, lng], 14);
3565                            window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng };
3566
3567                            // Calculate route if start marker exists
3568                            if (typeof mptbm_osm_start_marker !== 'undefined' && mptbm_osm_start_marker) {
3569                                mptbm_calculate_osm_distance();
3570                            }
3571                        }
3572                    } else if (typeof google !== 'undefined' && typeof google.maps !== 'undefined') {
3573                        if (typeof mptbm_map !== 'undefined' && mptbm_map) {
3574                            var latLng = new google.maps.LatLng(lat, lng);
3575                            if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) {
3576                                mptbm_end_marker.setMap(null);
3577                            }
3578                            mptbm_end_marker = new google.maps.Marker({
3579                                position: latLng,
3580                                map: mptbm_map,
3581                                title: locationLabel
3582                            });
3583                            mptbm_map.setCenter(latLng);
3584                            mptbm_map.setZoom(14);
3585                            window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng };
3586
3587
3588                            // Update distance cookie for Google Maps
3589                            var latLngStr = lat + "," + lng;
3590                            var startPlace = document.getElementById('mptbm_map_start_place');
3591                            mptbm_set_cookie_distance_duration(startPlace ? startPlace.value : '', latLngStr);
3592
3593                            // Calculate route if start marker exists
3594                            if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) {
3595                                mptbm_calculate_google_route_from_markers();
3596                            }
3597                        }
3598                    }
3599                }
3600            }
3601        } else {
3602            // Reset end coordinates if no place selected
3603            window.mptbm_fixed_zone_end_coords = null;
3604        }
3605
3606        if (price_based === 'manual' && end_place) {
3607            mptbm_update_manual_route_map(parent);
3608        }
3609    });
3610    $(document).on("change", "#mptbm_map_start_place,#mptbm_map_end_place", function () {
3611        let parent = $(this).closest(".mptbm_transport_search_area");
3612        mptbm_content_refresh(parent);
3613        let start_place = parent.find("#mptbm_map_start_place").val();
3614        let end_place = parent.find("#mptbm_map_end_place").val();
3615        if (start_place || end_place) {
3616            if (start_place) {
3617                mptbm_set_cookie_distance_duration(start_place);
3618                parent.find("#mptbm_map_end_place").focus();
3619            } else {
3620                mptbm_set_cookie_distance_duration(end_place);
3621                parent.find("#mptbm_map_start_place").focus();
3622            }
3623        } else {
3624            parent.find("#mptbm_map_start_place").focus();
3625        }
3626    }
3627    );
3628    $(document).on("change", ".mptbm_transport_search_area [name='mptbm_taxi_return']", function () {
3629        let parent = $(this).closest(".mptbm_transport_search_area");
3630        mptbm_content_refresh(parent);
3631    }
3632    );
3633    $(document).on(
3634        "change",
3635        ".mptbm_transport_search_area [name='mptbm_waiting_time']",
3636        function () {
3637            let parent = $(this).closest(".mptbm_transport_search_area");
3638            mptbm_content_refresh(parent);
3639        }
3640    );
3641})(jQuery);
3642
3643function mptbm_content_refresh(parent) {
3644    parent.find('[name="mptbm_post_id"]').val("");
3645    parent.find(".mptbm_map_search_result").remove();
3646    parent.find(".mptbm_order_summary").remove();
3647}
3648// Route planning + map now stay visible while results load inline below the
3649// map (instead of switching to a separate step/tab under a full dark
3650// overlay) - show a lightweight in-button spinner instead of blurring the
3651// whole panel.
3652// The AJAX response's outer wrapper carries data-tabs-next="#mptbm_search_result",
3653// which the shared tab-pane CSS (.tabsContentNext [data-tabs-next]) hides by
3654// default unless it has .active - normally added by switching to that tab.
3655// We're no longer switching tabs (results render inline below the map
3656// instead), so mark it active and drop the attribute directly.
3657// The OSM map keeps whatever zoom/center it had before its container was
3658// resized - Leaflet has no way to know the container changed size on its
3659// own, so a collapse/expand leaves it showing a cropped fragment of the
3660// route rather than re-fitting to it. invalidateSize() tells Leaflet to
3661// re-measure its container, then re-fitting to the route's own bounds (or
3662// the marker group, before a route line exists) zooms so the whole trip is
3663// visible at whatever height the map currently has.
3664function mptbm_refit_osm_map() {
3665    if (typeof mptbm_osm_map === 'undefined' || !mptbm_osm_map) {
3666        return;
3667    }
3668    setTimeout(function () {
3669        mptbm_osm_map.invalidateSize();
3670        if (typeof mptbm_osm_route !== 'undefined' && mptbm_osm_route) {
3671            mptbm_osm_map.fitBounds(mptbm_osm_route.getBounds().pad(0.1));
3672            return;
3673        }
3674        var markers = [
3675            typeof mptbm_osm_start_marker !== 'undefined' ? mptbm_osm_start_marker : null,
3676            typeof mptbm_osm_end_marker !== 'undefined' ? mptbm_osm_end_marker : null,
3677            typeof mptbm_osm_extra_marker !== 'undefined' ? mptbm_osm_extra_marker : null,
3678        ].filter(Boolean);
3679        if (markers.length > 0) {
3680            var group = new L.featureGroup(markers);
3681            mptbm_osm_map.fitBounds(group.getBounds().pad(0.1));
3682        }
3683    }, 320);
3684}
3685// Sets the "1 Enter Ride Details / 2 Choose a vehicle / 3 Place Order" step
3686// indicator (.tabListsNext, top of the whole booking flow) to reflect
3687// targetSelector as the current step - mirrors what mp_script.js's
3688// active_next_tab() does to these same classes/icons/text when its
3689// nextTab_next/nextTab_prev buttons are clicked, but written as an explicit,
3690// idempotent set instead of reusing that function directly: that one also
3691// slides .tabsContentNext panels open/closed, adjusts page scroll, and (via
3692// mp_all_content_change) unconditionally *toggles* each step's checkmark/
3693// class/text - fine for a single click, but calling it every time results
3694// load (including repeat searches) would flip the checkmark back and forth
3695// instead of staying put once a step is done.
3696function mptbm_set_step_active(parent, targetSelector) {
3697    var $stepList = parent.find('.tabListsNext').first();
3698    if (!$stepList.length) {
3699        return;
3700    }
3701    var $steps = $stepList.children('[data-tabs-target-next]');
3702    var targetIndex = $steps.filter('[data-tabs-target-next="' + targetSelector + '"]').index() + 1;
3703    if (targetIndex < 1) {
3704        return;
3705    }
3706    $steps.each(function (i) {
3707        var stepNum = i + 1;
3708        var $step = jQuery(this);
3709        $step.toggleClass('active', stepNum <= targetIndex);
3710        // Steps strictly before the current one are "done" - swap their
3711        // number for a checkmark (data-open-icon/-text vs data-close-icon/
3712        // -text, same attributes mp_script.js's content_*_change() read).
3713        var isDone = stepNum < targetIndex;
3714        var addClass = $step.data('add-class');
3715        if (addClass) {
3716            var $classTarget = $step.find('[data-class]').length ? $step.find('[data-class]') : $step;
3717            $classTarget.toggleClass(addClass, isDone);
3718        }
3719        var openIcon = $step.data('open-icon');
3720        var closeIcon = $step.data('close-icon');
3721        if (openIcon || closeIcon) {
3722            var $iconTarget = $step.find('[data-icon]');
3723            if (isDone) {
3724                $iconTarget.removeClass(openIcon).addClass(closeIcon);
3725            } else {
3726                $iconTarget.removeClass(closeIcon).addClass(openIcon);
3727            }
3728        }
3729        var openText = $step.data('open-text') != null ? $step.data('open-text').toString() : '';
3730        var closeText = $step.data('close-text') != null ? $step.data('close-text').toString() : '';
3731        if (openText || closeText) {
3732            $step.find('[data-text]').html(isDone ? closeText : openText);
3733        }
3734    });
3735}
3736function mptbm_reveal_inline_results(target) {
3737    target.find('[data-tabs-next]').addClass('active').removeAttr('data-tabs-next');
3738    target.addClass('mptbm_inline_results_active');
3739    // Results live inside .mptbm_map_area (a sibling of the collapsible map
3740    // body), taking over that same column. For "manual" pricing the whole
3741    // column starts display:none (no map at all) - force it visible now
3742    // that it has results to show. Then auto-collapse just the map body
3743    // (still toggleable back open) since results are the priority now.
3744    var $mapArea = target.closest('.mptbm_map_area');
3745    // Flat-rate/"manual" pricing has no real map at all - no geocoding, just
3746    // named location dropdowns. .mptbm_inline_search_results still lives
3747    // nested inside .mptbm_map_area though (see below), so that column still
3748    // needs to be revealed to show these results - just without any of the
3749    // map-specific chrome/collapse/refit logic, which would otherwise force
3750    // open a blank gray box sized for a map that was never there.
3751    //
3752    // Can't tell "no map" apart from "map not initialized yet" just by
3753    // checking mptbm_osm_map's truthiness - on the tabs page it's one shared
3754    // global var, so a map already initialized for a different tab (e.g. the
3755    // default "Hourly" tab on page load) stays truthy even after switching to
3756    // Flat Rate. Read this specific tab/form's own price_based value instead
3757    // - the same field the PHP template itself keys the map's display on.
3758    var $searchAreaRoot = $mapArea.closest('.mptbm_transport_search_area');
3759    var priceBased = $searchAreaRoot.find('input[name="mptbm_price_based"]').val();
3760    // Deliberately NOT reading data-map here - that's the shortcode/block's
3761    // own "map" option and only governs the pre-search form state. Once
3762    // results are revealed, visibility is instead driven by the separate
3763    // global admin switch (Map API Settings > Show Map on Search Result
3764    // Page), rendered into data-show-map-result in get_details.php. Without
3765    // this check, that setting was being silently ignored here and the map
3766    // always forced back to visible once results were shown.
3767    var showMapResult = ($mapArea.attr('data-show-map-result') || 'yes').toLowerCase();
3768    var manualMapEnabled = ($mapArea.attr('data-manual-map') || 'no').toLowerCase() === 'yes';
3769    var hasMap = (priceBased !== 'manual' || manualMapEnabled) && showMapResult !== 'no';
3770    // Results now show inline on step 1's own panel instead of switching to a
3771    // separate step-2 panel, but the step indicator above it should still
3772    // read as "Choose a vehicle" being current now that there's something to
3773    // choose from.
3774    mptbm_set_step_active($searchAreaRoot, '#mptbm_search_result');
3775    if ($mapArea.length) {
3776        if (hasMap) {
3777            // mptbm_map_collapsed toggles on/off as the user opens/closes the
3778            // map peek - mptbm_results_shown never comes back off, marking
3779            // "results are now sharing this column with the map" for CSS
3780            // that needs to tell that apart from the pre-search state (see
3781            // the .fullHeight height override below).
3782            $mapArea.css('display', 'flex').addClass('mptbm_map_collapsed mptbm_results_shown');
3783            var $toggle = $mapArea.find('.mptbm_map_collapse_toggle');
3784            $toggle.attr('aria-expanded', 'false');
3785            $toggle.find('[data-label]').text($toggle.data('expand-text'));
3786            mptbm_refit_osm_map();
3787        } else {
3788            $mapArea.css('display', 'flex').addClass('mptbm_map_area_no_map');
3789        }
3790        // The Total Distance/Total Time bar above the map is written from the
3791        // browser's own Directions result. The prices in these results were
3792        // calculated from a separate server-side lookup, which is allowed to
3793        // disagree with it - different provider (Google here, OSRM whenever the
3794        // server-side Google request is refused), so a different road and a
3795        // different number. Leaving the browser's figure on screen next to a
3796        // fare derived from the other one is how a trip ends up displaying
3797        // "11.9 km" while being quoted for 22.5 km. Once results exist, the
3798        // priced numbers win. Only overwritten when the server actually
3799        // verified a distance (choose_vehicles.php leaves these empty
3800        // otherwise), so the client-side estimate still stands on its own when
3801        // there was no server-side lookup to trust.
3802        var pricedDistanceText = target.find('.mptbm_priced_distance_text').first().val();
3803        var pricedDurationText = target.find('.mptbm_priced_duration_text').first().val();
3804        if (pricedDistanceText) {
3805            $mapArea.find('.mptbm_distance_time .mptbm_total_distance').text(pricedDistanceText);
3806        }
3807        if (pricedDurationText) {
3808            $mapArea.find('.mptbm_distance_time .mptbm_total_time').text(pricedDurationText);
3809        }
3810        // Fold Total Distance/Total Time into the trip-summary card's own
3811        // meta row too, alongside Duration/Pickup Date/Pickup Time - reading
3812        // the current text rather than moving the live .mptbm_total_distance/
3813        // .mptbm_total_time nodes themselves, since those keep getting
3814        // updated in place as the user adjusts locations for their *next*
3815        // search and need to still exist for that. Hiding (not removing)
3816        // the original bar keeps that live-update wiring intact underneath.
3817        // There can be several .mptbm_summary_top_row elements at once - one
3818        // per hidden .leftSidebar (summary.php renders one per pricing tab)
3819        // plus the one actually inside the visible .mptbm_results_toolbar -
3820        // .first() alone would silently grab a hidden one, so scope to the
3821        // toolbar's own row specifically.
3822        var $summaryRow = target.find('.mptbm_results_toolbar .mptbm_summary_top_row').first();
3823        if ($summaryRow.length) {
3824            // Transfer Type and Extra Waiting Hours are their own custom
3825            // dropdown widgets (a readonly text input showing the chosen
3826            // option's label, proxying a real <select>) living in Route
3827            // Planning itself (a sibling of .mptbm_map_area, both under the
3828            // same .mptbm_transport_search_area root) - only rendered at all
3829            // when their respective settings are enabled, so each is read
3830            // only if actually present.
3831            var $transferType = $searchAreaRoot.find('[data-proxy-for="mptbm_taxi_return"]');
3832            var $waitingHours = $searchAreaRoot.find('[data-proxy-for="mptbm_waiting_time"]');
3833            if ($transferType.length) {
3834                var transferLabel = $transferType.find('span').first().text().trim();
3835                var transferValue = $transferType.find('input.formControl').first().val();
3836                if (transferValue) {
3837                    $summaryRow.append(
3838                        jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_transfer_type"></div>')
3839                            .append(jQuery('<h6/>').text(transferLabel))
3840                            .append(jQuery('<p class="_textLight_1"/>').text(transferValue))
3841                    );
3842                }
3843            }
3844            if ($waitingHours.length) {
3845                var waitingLabel = $waitingHours.find('span').first().text().trim();
3846                var waitingValue = $waitingHours.find('input.formControl').first().val();
3847                if (waitingValue) {
3848                    $summaryRow.append(
3849                        jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_waiting_hours"></div>')
3850                            .append(jQuery('<h6/>').text(waitingLabel))
3851                            .append(jQuery('<p class="_textLight_1"/>').text(waitingValue))
3852                    );
3853                }
3854            }
3855        }
3856        // A manual route has distance/time only when its optional map resolved
3857        // both configured locations. In that case the same verified values are
3858        // folded into this summary just like every other mapped pricing mode.
3859        var $distanceTime = hasMap ? $mapArea.find('.mptbm_distance_time') : jQuery();
3860        if ($summaryRow.length && $distanceTime.length) {
3861            var $distanceVal = $distanceTime.find('.mptbm_total_distance').first();
3862            var $timeVal = $distanceTime.find('.mptbm_total_time').first();
3863            var distanceText = $distanceVal.text().trim();
3864            var timeText = $timeVal.text().trim();
3865            // The original bar's labels are written in ALL CAPS in the source
3866            // string itself (translated, so not just a CSS transform) - title-
3867            // casing here (rather than a CSS transform, which can't lowercase
3868            // characters that are already uppercase) matches the normal-case
3869            // style of Duration/Pickup Date/Pickup Time next to them.
3870            function mptbm_title_case(str) {
3871                return str.toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); });
3872            }
3873            var distanceLabel = mptbm_title_case($distanceVal.closest('.fdColumn').find('h6').first().text().trim());
3874            var timeLabel = mptbm_title_case($timeVal.closest('.fdColumn').find('h6').first().text().trim());
3875            if (distanceText) {
3876                $summaryRow.append(
3877                    jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_distance"></div>')
3878                        .append(jQuery('<h6/>').text(distanceLabel))
3879                        .append(jQuery('<p class="_textLight_1"/>').text(distanceText))
3880                );
3881            }
3882            if (timeText) {
3883                $summaryRow.append(
3884                    jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_time_total"></div>')
3885                        .append(jQuery('<h6/>').text(timeLabel))
3886                        .append(jQuery('<p class="_textLight_1"/>').text(timeText))
3887                );
3888            }
3889            $distanceTime.addClass('mptbm_distance_time_merged');
3890        }
3891    }
3892    // mp_script.js's lazy-load only scans [data-bg-image] on initial page
3893    // load/scroll (loadBgImage(), guarded so it only ever runs once) - vehicle
3894    // photos appended here afterwards never get picked up otherwise, showing
3895    // as the empty gray placeholder. loadBgImage() itself has no such guard,
3896    // so calling it directly force-scans the newly appended cards' images.
3897    if (typeof loadBgImage === 'function') {
3898        loadBgImage();
3899    }
3900}
3901function mptbm_search_loading(parent, isLoading) {
3902    var btn = parent.find('#mptbm_get_vehicle');
3903    var icon = btn.find('.fa-search-location, .fa-spinner').first();
3904    if (isLoading) {
3905        btn.prop('disabled', true).addClass('mptbm-searching');
3906        icon.removeClass('fa-search-location').addClass('fa-spinner fa-spin');
3907    } else {
3908        btn.prop('disabled', false).removeClass('mptbm-searching');
3909        icon.removeClass('fa-spinner fa-spin').addClass('fa-search-location');
3910    }
3911}
3912//=======================//
3913function mptbm_price_calculation(parent) {
3914    let target_summary = parent.find(".mptbm_transport_summary");
3915    let total = 0;
3916    let post_id = parseInt(parent.find('[name="mptbm_post_id"]').val());
3917    if (post_id > 0) {
3918        let quantityInput = parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`);
3919        let quantityVal = quantityInput.length ? parseInt(quantityInput.val()) || 1 : 1;
3920
3921        // Use the unit price of transport
3922        let unit_transport_price = parseFloat(parent.find('[name="mptbm_post_id"]').data("unit-transport-price") || parent.find('[name="mptbm_post_id"]').attr("data-price") / quantityVal || 0);
3923        let base_transport_price = unit_transport_price * quantityVal;
3924
3925        let unit_base_price_extra = parseFloat(parent.find('[name="mptbm_post_id"]').attr("data-unit-base-price") || 0);
3926        let tax_multiplier_val = parseFloat(parent.find('[name="mptbm_post_id"]').attr("data-tax-multiplier") || 1);
3927
3928
3929        let base_price_extra = unit_base_price_extra * quantityVal * tax_multiplier_val;
3930
3931        total = total + base_transport_price + base_price_extra;
3932
3933
3934        parent.find(".mptbm_extra_service_item").each(function () {
3935            let service_name = jQuery(this)
3936                .find('[name="mptbm_extra_service[]"]')
3937                .val();
3938            if (service_name) {
3939                let ex_target = jQuery(this).find('[name="mptbm_extra_service_qty[]"]'); // Added missing ] also
3940                let ex_qty = parseInt(ex_target.val());
3941                let ex_price = ex_target.data("price");
3942                ex_price = ex_price && ex_price > 0 ? ex_price : 0;
3943                total = total + parseFloat(ex_price) * ex_qty;
3944            }
3945        });
3946    }
3947    var el = target_summary.find(".mptbm_product_total_price");
3948    el.html(mp_price_format(total));
3949    // iOS DOM reflow workaround
3950    if (mptbm_is_ios()) {
3951        el.hide().show(0);
3952    }
3953}
3954
3955/**
3956 * Calculates distance from Base Location to Pickup and Dropoff to Base Location
3957 */
3958function mptbm_calculate_base_distances(settings, pickup, dropoff, callback) {
3959    if (!settings || !settings.coords || !pickup || !dropoff) {
3960        callback({ distance: 0, duration: 0 });
3961        return;
3962    }
3963
3964    // Check if we should use OSM (if Google is not defined or explicitly using OSM)
3965    // We check for mptbm_osm_map to see if OSM is the active map provider
3966    if (typeof google === 'undefined' || typeof mptbm_osm_map !== 'undefined') {
3967        mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback);
3968        return;
3969    }
3970
3971    let base_coords = settings.coords;
3972    let total_distance = 0;
3973    let total_duration = 0;
3974    let pending_calls = 0;
3975
3976    let check_complete = function () {
3977        if (pending_calls === 0) {
3978            callback({ distance: total_distance, duration: total_duration });
3979        }
3980    };
3981
3982    let calculate = function (origin, destination) {
3983        pending_calls++;
3984        let service = new google.maps.DistanceMatrixService();
3985        service.getDistanceMatrix({
3986            origins: [origin],
3987            destinations: [destination],
3988            travelMode: 'DRIVING',
3989        }, function (response, status) {
3990            if (status === 'OK' && response.rows[0].elements[0].status === 'OK') {
3991                total_distance += response.rows[0].elements[0].distance.value;
3992                total_duration += response.rows[0].elements[0].duration.value;
3993            }
3994            pending_calls--;
3995            check_complete();
3996        });
3997    };
3998
3999    if (settings.charge_pickup === 'yes') {
4000        calculate(base_coords, pickup);
4001    }
4002    if (settings.charge_dropoff === 'yes') {
4003        calculate(dropoff, base_coords);
4004    }
4005
4006    if (pending_calls === 0) {
4007        callback({ distance: 0, duration: 0 });
4008    }
4009}
4010(function ($) {
4011
4012    $(document).on('click', '.mp_quantity_minus, .mp_quantity_plus', function () {
4013        var postId = $(this).data('post-id');
4014        var $input = $(`.mp_quantity_input[data-post-id="${postId}"]`);
4015        var currentVal = parseInt($input.val());
4016        var maxVal = parseInt($input.attr('max'));
4017        var minVal = parseInt($input.attr('min'));
4018
4019        if ($(this).hasClass('mp_quantity_minus')) {
4020            if (currentVal > minVal) {
4021                $input.val(currentVal - 1);
4022            }
4023        } else {
4024            if (currentVal < maxVal) {
4025                $input.val(currentVal + 1);
4026            }
4027        }
4028
4029        var updatedVal = parseInt($input.val());
4030        var $parent = $(this).closest('.mptbm_booking_item');
4031        var $searchArea = $parent.closest('.mptbm_transport_search_area');
4032        var transportPrice = parseFloat($(`.mptbm_transport_select[data-post-id="${postId}"]`).attr('data-transport-price'));
4033        var $summary = $searchArea.find('.mptbm_transport_summary');
4034
4035        // Check if there's a custom message
4036        let customMessage = $parent.find('.mptbm-custom-price-message').html();
4037        if (customMessage) {
4038            // If there's a custom message, show it with quantity
4039            $summary.find('.mptbm_product_price').html(
4040                'x' + updatedVal + ' <span style="color:#000;">|&nbsp;&nbsp;</span>' + customMessage
4041            );
4042        } else {
4043            // If no custom message, show price as before
4044            $summary.find('.mptbm_product_price').html(
4045                'x' + updatedVal + ' <span style="color:#000;">|&nbsp;&nbsp;</span>' + mp_price_format(transportPrice * updatedVal)
4046            );
4047        }
4048
4049        // 🧠 Update the data-price dynamically if needed
4050        $searchArea.find('[name="mptbm_post_id"]').attr('data-price', transportPrice * updatedVal);
4051
4052        // ✅ Now update the total
4053        mptbm_price_calculation($searchArea);
4054    });
4055    $(document).on('click', '.mptbm_transport_search_area .mptbm_transport_select', function () {
4056        let $this = $(this);
4057        let postId = $this.data('post-id');
4058        let parent = $this.closest('.mptbm_transport_search_area');
4059
4060        let target_summary = parent.find('.mptbm_transport_summary');
4061        let target_extra_service = parent.find('.mptbm_extra_service');
4062        let target_extra_service_summary = parent.find('.mptbm_extra_service_summary');
4063        let all_quantity_selectors = parent.find('.mptbm_quantity_selector');
4064        let target_quantity_selector = parent.find('.mptbm_quantity_selector_' + postId);
4065
4066        if (target_quantity_selector.length && target_quantity_selector.hasClass('mptbm_booking_item_hidden')) {
4067            all_quantity_selectors.addClass('mptbm_booking_item_hidden');
4068            target_quantity_selector.removeClass('mptbm_booking_item_hidden');
4069        } else {
4070            all_quantity_selectors.addClass('mptbm_booking_item_hidden');
4071        }
4072
4073        target_summary.slideDown(400);
4074        target_extra_service.slideDown(400).html('');
4075        target_extra_service_summary.slideDown(400).html('');
4076        parent.find('[name="mptbm_post_id"]').val('');
4077        parent.find('.mptbm_checkout_area').html('');
4078
4079        if ($this.hasClass('active_select')) {
4080            $this.removeClass('active_select');
4081            mp_all_content_change($this);
4082            target_summary.slideUp(400);
4083            // No vehicle selected anymore -- step 3 is no longer a preview.
4084            // Matched by data-tabs-target-next rather than .step-place-order:
4085            // that class only exists in transport_result.php's copy of this
4086            // stepper, not the one registration_layout.php renders.
4087            parent.find('[data-tabs-target-next="#mptbm_order_summary"]').removeClass('active');
4088        } else {
4089            parent.find('.mptbm_transport_select.active_select').each(function () {
4090                $(this).removeClass('active_select');
4091                mp_all_content_change($(this));
4092            }).promise().done(function () {
4093                let transport_name = $this.attr('data-transport-name');
4094                let transport_price = parseFloat($this.attr('data-transport-price'));
4095                let post_id = $this.attr('data-post-id');
4096
4097                let quantityInput = parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`);
4098                let quantityVal = quantityInput.length ? parseInt(quantityInput.val()) || 1 : 1;
4099
4100                target_summary.find('.mptbm_product_name').html(transport_name);
4101
4102                let customMessage = $this.closest('.mptbm_booking_item').find('.mptbm-custom-price-message').html();
4103                if (customMessage) {
4104                    target_summary.find('.mptbm_product_price').html(
4105                        'x' + quantityVal + ' <span style="color:#000;">|&nbsp;&nbsp;</span> ' + customMessage
4106                    );
4107                } else {
4108                    target_summary.find('.mptbm_product_price').html(
4109                        'x' + quantityVal + ' <span style="color:#000;">|&nbsp;&nbsp;</span> ' + mp_price_format(transport_price * quantityVal)
4110                    );
4111                }
4112
4113                $this.addClass('active_select');
4114                $('.mptbm_booking_item').removeClass('selected');
4115                $this.closest('.mptbm_booking_item').addClass('selected');
4116
4117                // Preview step 3 ("Place Order") as active as soon as a vehicle is
4118                // picked, ahead of the checkout content actually loading.
4119                parent.find('[data-tabs-target-next="#mptbm_order_summary"]').addClass('active');
4120
4121                mp_all_content_change($this);
4122
4123                parent.find('[name="mptbm_post_id"]').val(post_id);
4124                parent.find('[name="mptbm_post_id"]').attr('data-price', transport_price * quantityVal);
4125                parent.find('[name="mptbm_post_id"]').attr('data-unit-transport-price', transport_price);
4126                parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated', 0);
4127                parent.find('[name="mptbm_post_id"]').attr('data-unit-base-price', 0);
4128
4129                // --- BASE PRICE CALCULATION ---
4130                // FIX: Use the server-calculated base price directly to avoid discrepancies (1.30 difference)
4131                // The server has already calculated this using high-precision coordinates and settings.
4132                // We should trust it instead of re-calculating on the client side which might use slightly different logic/API.
4133
4134                let calcBasePrice = function (callback) {
4135                    let server_base_price = parseFloat($this.attr('data-unit-base-price') || 0);
4136                    callback(server_base_price);
4137                };
4138
4139                calcBasePrice(function (base_p) {
4140                    parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated', base_p * quantityVal);
4141                    parent.find('[name="mptbm_post_id"]').attr('data-unit-base-price', base_p);
4142                    let total_b = base_p * quantityVal;
4143
4144                    // Update the new inline base price detail in summary.php
4145                    let detail_container = parent.find('.mptbm_base_price_detail');
4146                    if (base_p > 0) {
4147                        let b_html = '<div class="_textTheme" style="font-size: 13px; margin-top: 5px; padding-left: 25px;">' +
4148                            'Base Price: ' + mp_price_format(total_b) + '</div>';
4149                        detail_container.html(b_html).show();
4150                    } else {
4151                        detail_container.html('').hide();
4152                    }
4153
4154                    mptbm_price_calculation(parent);
4155                });
4156
4157                // Fetch extra services + their summary in parallel instead of
4158                // one after another - each request only needs post_id and
4159                // renders its own independent template server-side (see
4160                // get_mptbm_extra_service/_summary in MPTBM_Transport_Search.php),
4161                // so chaining them was just doubling the wait for no reason.
4162                dLoader(parent.find('.tabsContentNext'));
4163                var extraServiceReq = $.ajax({
4164                    type: 'POST',
4165                    url: mp_ajax_url,
4166                    data: { "action": "get_mptbm_extra_service", "nonce": mptbm_ajax.search_nonce, "post_id": post_id }
4167                });
4168                var extraServiceSummaryReq = $.ajax({
4169                    type: 'POST',
4170                    url: mp_ajax_url,
4171                    data: { "action": "get_mptbm_extra_service_summary", "nonce": mptbm_ajax.search_nonce, "post_id": post_id }
4172                });
4173                $.when(extraServiceReq, extraServiceSummaryReq).done(function (serviceResp, summaryResp) {
4174                    target_extra_service.html(serviceResp[0]);
4175                    // The footer copy of .mptbm_transport_summary just arrived with
4176                    // this markup - CSS hides every .mptbm_transport_summary by
4177                    // default (shown only via the sidebar's own slideDown, which
4178                    // already ran before this element existed), so reveal this one
4179                    // explicitly rather than waiting on the next price recalculation.
4180                    target_extra_service.find('.mptbm_transport_summary').show();
4181                    // Fill in its price/total immediately - otherwise it stays blank
4182                    // until the customer's first extra-service click, since the vehicle
4183                    // price line and base price calculation both already ran before
4184                    // this copy existed. Copy the sidebar's already-computed price line
4185                    // rather than re-deriving it (quantity/tax/custom-message logic
4186                    // lives in the click handler above, not worth duplicating here).
4187                    target_extra_service.find('.mptbm_product_price').html(
4188                        target_summary.find('.mptbm_product_price').first().html()
4189                    );
4190                    mptbm_price_calculation(parent);
4191                    checkAndToggleBookNowButton(parent);
4192                    if (mptbm_is_ios()) {
4193                        target_extra_service[0].style.display = 'none';
4194                        void target_extra_service[0].offsetHeight;
4195                        target_extra_service[0].style.display = '';
4196                    }
4197
4198                    // Re-query instead of using target_extra_service_summary as
4199                    // captured before this vehicle's markup existed: that snapshot
4200                    // only ever matched the sidebar copy, so the footer copy
4201                    // (revealed just above) would never receive the itemized
4202                    // breakdown otherwise.
4203                    let all_extra_service_summary = parent.find('.mptbm_extra_service_summary');
4204                    all_extra_service_summary.html(summaryResp[0]).promise().done(function () {
4205                        if (target_extra_service.find('[name="mptbm_extra_service[]"]').length > 0) {
4206                            target_summary.slideDown(400);
4207                            target_extra_service.slideDown(400);
4208                            all_extra_service_summary.slideDown(400);
4209                            pageScrollTo(target_extra_service);
4210                        }
4211                        dLoaderRemove(parent.find('.tabsContentNext'));
4212                        if (!target_extra_service.find('[name="mptbm_extra_service[]"]').length) {
4213                            parent.find('.mptbm_book_now[type="button"]').trigger('click');
4214                        } else {
4215                            checkAndToggleBookNowButton(parent);
4216                        }
4217                        if (mptbm_is_ios()) {
4218                            all_extra_service_summary.hide().show(0);
4219                        }
4220                    });
4221                });
4222            });
4223        }
4224    });
4225    $(document).on('click', '.mptbm_transport_search_area .mptbm_price_calculation', function () {
4226        mptbm_price_calculation($(this).closest('.mptbm_transport_search_area'));
4227    });
4228    //========Extra service==============//
4229    $(document).on('change', '.mptbm_transport_search_area [name="mptbm_extra_service_qty[]"]', function () {
4230        $(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service[]"]').trigger('change');
4231        let parent = $(this).closest('.mptbm_transport_search_area');
4232        checkAndToggleBookNowButton(parent);
4233    });
4234    $(document).on('change', '.mptbm_transport_search_area [name="mptbm_extra_service[]"]', function () {
4235        let parent = $(this).closest('.mptbm_transport_search_area');
4236        let service_name = $(this).data('value');
4237        let service_value = $(this).val();
4238        if (service_value) {
4239            let qty = $(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service_qty[]"]').val();
4240            parent.find('[data-extra-service="' + service_name + '"]').slideDown(350).find('.ex_service_qty').html('x' + qty);
4241        } else {
4242            parent.find('[data-extra-service="' + service_name + '"]').slide
4242Up(350);
4243        }
4244        mptbm_price_calculation(parent);
4245
4246        checkAndToggleBookNowButton(parent);
4247    });
4248
4249    function checkAndToggleBookNowButton(parent) {
4250        // Check if there are any extra services present
4251        let extraServicesAvailable = parent.find('[name="mptbm_extra_service[]"]').length > 0;
4252
4253        if (extraServicesAvailable) {
4254            parent.find('.mptbm_book_now[type="button"]').show();
4255        } else {
4256            parent.find('.mptbm_book_now[type="button"]').hide();
4257        }
4258    }
4259
4260
4261
4262    //===========================//
4263    $(document).on('click', '.mptbm_map_collapse_toggle', function () {
4264        var $btn = $(this);
4265        var $mapArea = $btn.closest('.mptbm_map_area');
4266        var collapsed = $mapArea.toggleClass('mptbm_map_collapsed').hasClass('mptbm_map_collapsed');
4267        $btn.attr('aria-expanded', collapsed ? 'false' : 'true');
4268        $btn.find('[data-label]').text(collapsed ? $btn.data('expand-text') : $btn.data('collapse-text'));
4269        mptbm_refit_osm_map();
4270    });
4271
4272    //===========================//
4273    $(document).on('click', '.mptbm_inline_results_reset', function () {
4274        window.location.reload();
4275    });
4276
4277    //===========================//
4278    $(document).on('click', '.mptbm_transport_search_area .mptbm_get_vehicle_prev', function () {
4279        var mptbmTemplateExists = $(".mptbm-show-search-result").length;
4280        if (mptbmTemplateExists) {
4281            // Function to retrieve cookie value by name
4282            function getCookie(name) {
4283                // Split the cookies by semicolon
4284                var cookies = document.cookie.split(";");
4285                // Loop through each cookie to find the one with the specified name
4286                for (var i = 0; i < cookies.length; i++) {
4287                    var cookie = cookies[i].trim();
4288                    // Check if the cookie starts with the specified name
4289                    if (cookie.startsWith(name + "=")) {
4290                        // Return the value of the cookie
4291                        return cookie.substring(name.length + 1);
4292                    }
4293                }
4294                // Return null if the cookie is not found
4295                return null;
4296            }
4297            // Usage example:
4298            var httpReferrerValue = getCookie("httpReferrer");
4299            // Function to delete a cookie by setting its expiry date to a past time
4300            function deleteCookie(name) {
4301                document.cookie =
4302                    name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
4303            }
4304            deleteCookie("httpReferrer");
4305            window.location.href = httpReferrerValue;
4306        } else {
4307            let parent = $(this).closest(".mptbm_transport_search_area");
4308            parent.find(".get_details_next_link").slideDown("fast");
4309            parent.find(".nextTab_prev").trigger("click");
4310        }
4311    });
4312    $(document).on('click', '.mptbm_transport_search_area .mptbm_summary_prev', function () {
4313        let mptbmTemplateExists = $(".mptbm-show-search-result").length;
4314        if (mptbmTemplateExists) {
4315            $(".mptbm_order_summary").css("display", "none");
4316            $(".mptbm_map_search_result").css("display", "block").hide().slideDown("slow");
4317            $(".step-place-order").removeClass("active");
4318        } else {
4319            let parent = $(this).closest(".mptbm_transport_search_area");
4320            parent.find(".nextTab_prev").trigger("click");
4321        }
4322    });
4323    //===========================//
4324    $(document).on("click", ".mptbm_book_now[type='button']", function () {
4325        if ($(this).is(':disabled')) {
4326            // Booking is unavailable (no WooCommerce and no Pro plugin active);
4327            // the disabled state can still be reached via the auto-click that
4328            // fires when a vehicle has no extra services, so bail out here too.
4329            return;
4330        }
4331        let parent = $(this).closest('.mptbm_transport_search_area');
4332        let target_checkout = parent.find('.mptbm_checkout_area');
4333        let start_place = parent.find('[name="mptbm_start_place"]').val();
4334        let end_place = parent.find('[name="mptbm_end_place"]').val();
4335        let mptbm_waiting_time = parent.find('[name="mptbm_waiting_time"]').val();
4336        let mptbm_taxi_return = parent.find('[name="mptbm_taxi_return"]').val();
4337        let return_target_date = parent.find("#mptbm_map_return_date").val();
4338        let return_target_time = parent.find("#mptbm_map_return_time").val();
4339        let mptbm_fixed_hours = parent.find('[name="mptbm_fixed_hours"]').val();
4340        let post_id = parent.find('[name="mptbm_post_id"]').val();
4341        let date = parent.find('[name="mptbm_date"]').val();
4342        let link_id = $(this).attr('data-wc_link_id');
4343        let quantity = parseInt(parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`).val()) || 1;
4344        let mptbm_original_price_base = parent.find('[name="mptbm_original_price_base"]').val();
4345        let mptbm_threshold_base_price = parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated') || 0;
4346        // Generated with the vehicle-result response, so it remains fresh even when a
4347        // guest received the outer booking page from a full-page cache.
4348        let add_to_cart_nonce = parent.find('[name="mptbm_add_to_cart_nonce"]').val() || '';
4349
4350        if (start_place !== '' && end_place !== '' && link_id && post_id) {
4351            let extra_service_name = {};
4352            let extra_service_qty = {};
4353            let count = 0;
4354            parent.find('[name="mptbm_extra_service[]"]').each(function () {
4355                let ex_name = $(this).val();
4356                if (ex_name) {
4357                    extra_service_name[count] = ex_name;
4358                    let ex_qty = parseInt($(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service_qty[]"]').val());
4359                    ex_qty = ex_qty > 0 ? ex_qty : 1;
4360                    extra_service_qty[count] = ex_qty;
4361                    count++;
4362                }
4363            });
4364
4365            // Get coordinates for fixed_zone/fixed_zone_dropoff pricing
4366            let start_place_coordinates = null;
4367            let end_place_coordinates = null;
4368
4369            // Try to get from hidden inputs first
4370            let start_coords_input = parent.find('input[name="mptbm_start_place_coordinates"]');
4371            let end_coords_input = parent.find('input[name="mptbm_end_place_coordinates"]');
4372
4373            if (start_coords_input.length && start_coords_input.val()) {
4374                try {
4375                    start_place_coordinates = JSON.parse(start_coords_input.val());
4376                } catch (e) {
4377                    start_place_coordinates = start_coords_input.val();
4378                }
4379            } else if (typeof window.mptbm_fixed_zone_start_coords !== 'undefined' && window.mptbm_fixed_zone_start_coords) {
4380                start_place_coordinates = window.mptbm_fixed_zone_start_coords;
4381            } else if (mptbm_start_marker) {
4382                let pos = mptbm_start_marker.getPosition();
4383                if (pos) {
4384                    start_place_coordinates = { latitude: pos.lat(), longitude: pos.lng() };
4385                }
4386            }
4387
4388            if (end_coords_input.length && end_coords_input.val()) {
4389                try {
4390                    end_place_coordinates = JSON.parse(end_coords_input.val());
4391                } catch (e) {
4392                    end_place_coordinates = end_coords_input.val();
4393                }
4394            } else if (typeof window.mptbm_fixed_zone_end_coords !== 'undefined' && window.mptbm_fixed_zone_end_coords) {
4395                end_place_coordinates = window.mptbm_fixed_zone_end_coords;
4396            } else if (mptbm_end_marker) {
4397                let pos = mptbm_end_marker.getPosition();
4398                if (pos) {
4399                    end_place_coordinates = { latitude: pos.lat(), longitude: pos.lng() };
4400                }
4401            }
4402
4403            $.ajax({
4404                type: 'POST',
4405                url: mp_ajax_url,
4406                data: {
4407                    action: "mptbm_add_to_cart",
4408                    mptbm_add_to_cart_nonce: add_to_cart_nonce,
4409                    // Kept for one rolling-deployment cycle so an older PHP handler can
4410                    // still validate clients whose assets update before the backend.
4411                    nonce: mptbm_ajax.search_nonce,
4412                    //"product_id": post_id,
4413                    transport_quantity: quantity,
4414                    link_id: link_id,
4415                    mptbm_start_place: start_place,
4416                    mptbm_end_place: end_place,
4417                    mptbm_waiting_time: mptbm_waiting_time,
4418                    mptbm_taxi_return: mptbm_taxi_return,
4419                    mptbm_fixed_hours: mptbm_fixed_hours,
4420                    mptbm_date: date,
4421                    mptbm_return_date: return_target_date,
4422                    mptbm_return_time: return_target_time,
4423                    mptbm_extra_service: extra_service_name,
4424                    mptbm_extra_service_qty: extra_service_qty,
4425                    mptbm_passengers: parent.find('#mptbm_passengers').val(),
4426                    mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(),
4427                    mptbm_max_bag: parent.find('#mptbm_max_bag').val(),
4428                    mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(),
4429                    mptbm_extra_stop_place: parent.find('input[name="mptbm_extra_stop_place"]').val(),
4430                    mptbm_original_price_base: mptbm_original_price_base,
4431                    mptbm_distance: parent.find('input[name="mptbm_hidden_distance"]').val(),
4432                    mptbm_duration: parent.find('input[name="mptbm_hidden_duration"]').val(),
4433                    mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(),
4434                    start_place_coordinates: start_place_coordinates ? JSON.stringify(start_place_coordinates) : '',
4435                    end_place_coordinates: end_place_coordinates ? JSON.stringify(end_place_coordinates) : '',
4436                    mptbm_threshold_base_price: mptbm_threshold_base_price,
4437                    // Standalone (no-WooCommerce) Pro custom booking flow fields. Ignored
4438                    // by the WooCommerce add-to-cart handler when WooCommerce is active.
4439                    mptbm_payment_method: parent.find('[name="mptbm_payment_method"]:checked').val() || '',
4440                    mptbm_billing_name: parent.find('[name="mptbm_billing_name"]').val() || '',
4441                    mptbm_billing_email: parent.find('[name="mptbm_billing_email"]').val() || '',
4442                    mptbm_billing_phone: parent.find('[name="mptbm_billing_phone"]').val() || ''
4443                },
4444                beforeSend: function () {
4445                    dLoader(parent.find('.tabsContentNext'));
4446                },
4447                success: function (data) {
4448                    if ($('<div />', { html: data }).find("div").length > 0) {
4449                        var mptbmTemplateExists = $(".mptbm-show-search-result").length;
4450                        if (mptbmTemplateExists) {
4451                            $(".mptbm_map_search_result").css("display", "none");
4452                            $(".mptbm_order_summary").css("display", "block");
4453                            $(".step-place-order").addClass('active');
4454                        }
4455                        target_checkout.html(data).promise().done(function () {
4456                            target_checkout.find('.woocommerce-billing-fields .required').each(function () {
4457                                $(this).closest('p').find('.input-text , select, textarea ').attr('required', 'required');
4458                            });
4459                            $(document.body).trigger('init_checkout');
4460                            if ($('body select#billing_country').length > 0) {
4461                                $('body select#billing_country').select2({});
4462                            }
4463                            if ($('body select#billing_state').length > 0) {
4464                                $('body select#billing_state').select2({});
4465                            }
4466                            dLoaderRemove(parent.find('.tabsContentNext'));
4467                            parent.find('.nextTab_next').trigger('click');
4468                            // iOS DOM reflow workaround
4469                            if (mptbm_is_ios()) {
4470                                target_checkout[0].style.display = 'none';
4471                                void target_checkout[0].offsetHeight;
4472                                target_checkout[0].style.display = '';
4473                            }
4474                        });
4475                    } else if (data && /^https?:\/\//i.test(data.trim())) {
4476                        window.location.href = data;
4477                    } else {
4478                        // Empty/invalid response (e.g. both WooCommerce and a
4479                        // custom payment method were enabled and the request
4480                        // fell through without a usable result) - clear the
4481                        // loader instead of leaving the button stuck.
4482                        dLoaderRemove(parent.find('.tabsContentNext'));
4483                        console.log('mptbm_add_to_cart: unexpected response', data);
4484                    }
4485                },
4486                error: function (response) {
4487                    dLoaderRemove(parent.find('.tabsContentNext'));
4488                    console.log(response);
4489                }
4490            });
4491        } else {
4492            // Missing required data - bail out loud instead of leaving the button
4493            // looking clicked with no visible feedback and nothing in the console.
4494            console.warn('mptbm_book_now: missing required booking data, not submitting', {
4495                start_place: start_place, end_place: end_place, link_id: link_id, post_id: post_id
4496            });
4497        }
4498    });
4499
4500
4501
4502    $(document).ready(function () {
4503        let $tabs = $('.tab-link');
4504        let count = $tabs.length;
4505
4506        // Reset previous border-radius styles
4507        $tabs.css({
4508            'border-radius': '', // Clears any previously applied styles
4509        });
4510
4511        if (count === 1) {
4512            // If only one element, apply radius to all sides
4513            $tabs.eq(0).css('border-radius', 'var(--dbrl)');
4514        } else if (count >= 2) {
4515            // If three or more, apply left radius to first and right radius to third
4516            /*$tabs.eq(0).css({
4517                'border-top-left-radius': 'var(--dbrl)',
4518                'border-bottom-left-radius': 'var(--dbrl)'
4519            });
4520            $tabs.last().css({
4521                'border-top-right-radius': 'var(--dbrl)',
4522                'border-bottom-right-radius': 'var(--dbrl)'
4523            });*/
4524        }
4525        $('.mptb-tabs li').click(function () {
4526            var tab_id = $(this).attr('mptbm-data-tab');
4527            var form_style = $(this).attr('mptbm-data-form-style');
4528            var map = $(this).attr('mptbm-data-map');
4529            var $tabContentWrap = $(this).closest('.mptb-tab-container').find('.mptb-tabs-content-wrap');
4530            var vehicle_id = parseInt($(this).closest('.mptbm_transport_search_area').find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0;
4531
4532            // Ignore re-clicks on the active tab or while a switch is already loading
4533            if ($(this).hasClass('current') || $tabContentWrap.hasClass('mptbm-tab-loading')) {
4534                return;
4535            }
4536
4537            // Clean up existing map instance before switching tabs
4538            mptbm_cleanup_map();
4539
4540            // Freeze height so the wrap does not collapse while content stays
4541            // visible (faded) under the spinner. After load, settleHeight()
4542            // eases to the new content size, then clears the lock (height: auto).
4543            var lockedHeight = $tabContentWrap.outerHeight() || 0;
4544            if (lockedHeight > 0) {
4545                $tabContentWrap.css('min-height', lockedHeight + 'px');
4546            }
4547
4548            function settleHeight() {
4549                if (!$tabContentWrap.length) {
4550                    return;
4551                }
4552                var $currentTab = $tabContentWrap.find('.mptb-tab-content.current');
4553                var contentHeight = $currentTab.length ? $currentTab.outerHeight(true) : 0;
4554                if (contentHeight > 0) {
4555                    $tabContentWrap.css('min-height', contentHeight + 'px');
4556                }
4557                setTimeout(function () {
4558                    $tabContentWrap.css('min-height', '');
4559                }, 260);
4560            }
4561
4562            function clearTabLoader() {
4563                $tabContentWrap.removeClass('mptbm-tab-loading');
4564                $tabContentWrap.find('.mptbm-tab-loader, .mptbm-loading-overlay').remove();
4565            }
4566
4567            // Keep the active tab content on screen (opacity via CSS) and show
4568            // a spinner with no overlay background while the next tab loads.
4569            clearTabLoader();
4570            $tabContentWrap.addClass('mptbm-tab-loading');
4571            $tabContentWrap.append('<div class="mptbm-tab-loader"><div class="mptbm-spinner"></div></div>');
4572
4573            // Mark the clicked tab pill as active
4574            $('.mptb-tabs li').removeClass('current');
4575            $(this).addClass('current');
4576
4577            $.ajax({
4578                type: "POST",
4579                url: mp_ajax_url,
4580                data: {
4581                    action: "load_get_details_page",
4582                    nonce: mptbm_ajax.search_nonce,
4583                    tab_id: tab_id,
4584                    form_style: form_style,
4585                    map: map,
4586                    vehicle_id: vehicle_id
4587                },
4588                success: function (data) {
4589                    var tabContainer = $("#" + tab_id);
4590                    if (tabContainer.length === 0) {
4591                        var tabContainerParent = $tabContentWrap.length ? $tabContentWrap : $('.mptb-tab-container');
4592                        if (tabContainerParent.length > 0) {
4593                            var newTabContainer = $('<div id="' + tab_id + '" class="mptb-tab-content"></div>');
4594                            tabContainerParent.append(newTabContainer);
4595                            tabContainer = newTabContainer;
4596                        } else {
4597                            console.error('Tab container parent not found');
4598                            clearTabLoader();
4599                            return;
4600                        }
4601                    }
4602
4603                    // Empty other tabs only now — keeps previous content visible
4604                    // under the faded loader until replacement is ready, and
4605                    // avoids duplicate field IDs once the new markup is inserted.
4606                    $('.mptb-tab-content').not(tabContainer).empty();
4607                    tabContainer.html(data);
4608
4609                    $('.mptb-tab-content').removeClass('current');
4610                    tabContainer.addClass('current');
4611
4612                    if (!tabContainer.is(':visible')) {
4613                        tabContainer.css('display', 'block');
4614                    }
4615
4616                    clearTabLoader();
4617                    settleHeight();
4618
4619                    setTimeout(function () {
4620                        var currentTab = $('.mptb-tabs li.current').attr('mptbm-data-tab');
4621                        var mapEnabled = $('.mptb-tabs li.current').attr('mptbm-data-map');
4622
4623                        if (mapEnabled === 'yes' && mptbm_get_current_map_wrap() && mptbm_get_current_map_wrap().style.display !== 'none') {
4624                            mptbm_map_area_init();
4625                        }
4626
4627                        var mapType = document.getElementById('mptbm_map_type');
4628                        if (!(mapType && mapType.value === 'openstreetmap')) {
4629                            initializeGooglePlacesAutocomplete();
4630                        }
4631
4632                        settleHeight();
4633                    }, 100);
4634                },
4635                error: function () {
4636                    clearTabLoader();
4637                    var tabContainer = $("#" + tab_id);
4638                    if (tabContainer.length > 0) {
4639                        $('.mptb-tab-content').not(tabContainer).empty();
4640                        tabContainer.html('<div style="text-align: center; padding: 20px; color: red;"><p>Error loading content. Please try again.</p></div>');
4641                        $('.mptb-tab-content').removeClass('current');
4642                        tabContainer.addClass('current');
4643                    }
4644                    settleHeight();
4645                },
4646            });
4647        });
4648    });
4649
4650    // Handle select dropdown search functionality
4651    $(document).on('click', '#mptbm_manual_start_place, #mptbm_manual_end_place', function (e) {
4652
4653
4654        var $select = $(this);
4655        var selectId = $select.attr('id');
4656
4657
4658        // Remove any existing custom search elements
4659        $('.mptbm-custom-select-wrapper').remove();
4660
4661        // Check if select has options (dropoff might be empty initially)
4662        var $options = $select.find('option:not([disabled])');
4663
4664        if ($options.length <= 0) {
4665            return;
4666        }
4667
4668        // Get select position and dimensions
4669        var selectOffset = $select.offset();
4670        var selectWidth = $select.outerWidth();
4671        var selectHeight = $select.outerHeight();
4672
4673        // Keep the original select visible - don't hide it
4674        // $select.hide(); // REMOVED - keep select visible
4675
4676        // Create custom select wrapper with dynamic positioning
4677        // (cosmetic styling lives in mptbm_registration.css; only positioning is set inline)
4678        var $customWrapper = $('<div class="mptbm-custom-select-wrapper"></div>');
4679
4680        // Create search input
4681        var $searchInput = $('<input type="text" class="mptbm-custom-search-input" placeholder="Search locations..." />');
4682
4683        // Create options container
4684        var $optionsContainer = $('<div class="mptbm-custom-options"></div>');
4685
4686        // Function to update dropdown position
4687        function updateDropdownPosition() {
4688            var currentOffset = $select.offset();
4689            var currentWidth = $select.outerWidth();
4690            var currentHeight = $select.outerHeight();
4691
4692            var scrollTop = $(window).scrollTop();
4693            var scrollLeft = $(window).scrollLeft();
4694
4695            // Always position below
4696            var top = currentOffset.top - scrollTop + currentHeight + 2;
4697            var left = currentOffset.left - scrollLeft;
4698            var width = currentWidth;
4699
4700            var windowHeight = $(window).height();
4701            var windowWidth = $(window).width();
4702
4703            // Calculate available space and adjust dropdown height accordingly
4704            var availableHeight = windowHeight - top - 20; // 20px padding from bottom
4705            var maxHeight = Math.min(250, Math.max(100, availableHeight)); // Min 100px, max 250px
4706
4707            $optionsContainer.css('max-height', maxHeight + 'px');
4708
4709            // Prevent offscreen left/right
4710            if (left + width > windowWidth - 10) {
4711                left = windowWidth - width - 10;
4712            }
4713            if (left < 10) left = 10;
4714
4715            $customWrapper.css({
4716                top: top + 'px',
4717                left: left + 'px',
4718                width: width + 'px'
4719            });
4720        }
4721
4722        // Get all options from original select (excluding disabled ones)
4723        var $originalOptions = $select.find('option:not([disabled])');
4724        var optionsHtml = '';
4725
4726
4727        $originalOptions.each(function () {
4728            var optionText = $(this).text();
4729            var optionValue = $(this).val();
4730            var isSelected = $(this).is(':selected');
4731
4732            var selectedClass = isSelected ? 'mptbm-option-selected' : '';
4733            optionsHtml += '<div class="mptbm-custom-option ' + selectedClass + '" data-value="' + optionValue + '">' + optionText + '</div>';
4734        });
4735
4736        $optionsContainer.html(optionsHtml);
4737
4738        // Assemble and append to mptbm_transport_search_area
4739        $customWrapper.append($searchInput).append($optionsContainer);
4740        $('.mptbm_transport_search_area').append($customWrapper);
4741
4742        // Initial and responsive position
4743        updateDropdownPosition();
4744        $(window).on('scroll resize', updateDropdownPosition);
4745
4746        // Ensure map elements are not affected by the dropdown
4747        $('.mptbm_map_area').css('z-index', '1');
4748        $('.mptbm_map_area #mptbm_map_area').css('z-index', '1');
4749
4750        // Focus on search input
4751        $searchInput.focus();
4752
4753        // Handle search input
4754        $searchInput.on('input', function () {
4755            var searchTerm = $(this).val().toLowerCase();
4756            var $options = $customWrapper.find('.mptbm-custom-option');
4757
4758
4759            $options.each(function () {
4760                var optionText = $(this).text().toLowerCase();
4761                if (optionText.includes(searchTerm) || searchTerm === '') {
4762                    $(this).show();
4763                } else {
4764                    $(this).hide();
4765                }
4766            });
4767        });
4768
4769        // Handle option selection
4770        $customWrapper.on('click', '.mptbm-custom-option', function () {
4771            var selectedValue = $(this).data('value');
4772            var selectedText = $(this).text();
4773
4774            // Update original select
4775            $select.val(selectedValue);
4776            $select.trigger('change');
4777
4778            // Update search input with selected text
4779            $searchInput.val(selectedText);
4780
4781            // Remove custom wrapper (select stays visible)
4782            $customWrapper.remove();
4783
4784            // Restore map z-index
4785            $('.mptbm_map_area').css('z-index', '');
4786            $('.mptbm_map_area #mptbm_map_area').css('z-index', '');
4787
4788        });
4789
4790        // Handle select change event to clean up custom dropdown
4791        $select.one('change', function () {
4792            $customWrapper.remove();
4793            // Restore map z-index
4794            $('.mptbm_map_area').css('z-index', '');
4795            $('.mptbm_map_area #mptbm_map_area').css('z-index', '');
4796        });
4797
4798        // Handle clicking outside to close
4799        $(document).one('click', function (e) {
4800            if (!$(e.target).closest('.mptbm-custom-select-wrapper, #' + selectId).length) {
4801                $customWrapper.remove();
4802                // Restore map z-index
4803                $('.mptbm_map_area').css('z-index', '');
4804                $('.mptbm_map_area #mptbm_map_area').css('z-index', '');
4805            }
4806        });
4807
4808        // Handle window resize to update dropdown position with debouncing
4809        var positionUpdateTimeout;
4810        var positionUpdateHandler = function () {
4811            clearTimeout(positionUpdateTimeout);
4812            positionUpdateTimeout = setTimeout(function () {
4813                updateDropdownPosition();
4814            }, 16); // ~60fps throttling
4815        };
4816
4817        $(window).on('resize.mptbm-dropdown', positionUpdateHandler);
4818
4819        // Clean up event listeners when dropdown is removed
4820        var originalRemove = $customWrapper.remove;
4821        $customWrapper.remove = function () {
4822            clearTimeout(positionUpdateTimeout);
4823            $(window).off('resize.mptbm-dropdown');
4824            $(window).off('scroll resize', updateDropdownPosition);
4825            return originalRemove.call(this);
4826        };
4827
4828        // Handle escape key
4829        $searchInput.on('keydown', function (e) {
4830            if (e.key === 'Escape') {
4831                $customWrapper.remove();
4832                // Restore map z-index
4833                $('.mptbm_map_area').css('z-index', '');
4834                $('.mptbm_map_area #mptbm_map_area').css('z-index', '');
4835            }
4836        });
4837    });
4838
4839    // Prevent native dropdown behavior for manual select elements
4840    $(document).on('focus mousedown keydown', '#mptbm_manual_start_place, #mptbm_manual_end_place', function (e) {
4841        // Only prevent if it's not already handled by our custom dropdown
4842        if (!$(e.target).closest('.mptbm-custom-select-wrapper').length) {
4843            if (e.type === 'focus' || e.type === 'mousedown' ||
4844                (e.type === 'keydown' && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown' || e.key === 'ArrowUp'))) {
4845                e.preventDefault();
4846                e.stopPropagation();
4847            }
4848        }
4849    });
4850
4851    // "View Details" toggle - expands the real specs/reviews panel rendered
4852    // server-side in vehicle_item.php (only present when there is genuinely
4853    // more content than the visible feature chips).
4854    $(document).on('click', '.mptbm_view_details_toggle', function (e) {
4855        e.preventDefault();
4856        e.stopPropagation();
4857
4858        var $button = $(this);
4859        var postId = $button.data('post-id');
4860        var $wrapper = $button.closest('.mptbm-vehicle-wrapper');
4861        var $panel = $wrapper.find('.mptbm_vehicle_details_panel[data-post-id="' + postId + '"]');
4862        if (!$panel.length) {
4863            return;
4864        }
4865
4866        $panel.slideToggle(250, function () {
4867            var isOpen = $panel.is(':visible');
4868            $button.attr('aria-expanded', isOpen ? 'true' : 'false');
4869            $button.toggleClass('is-open', isOpen);
4870            $button.find('[data-label]').text(isOpen ? $button.data('hide-text') : $button.data('view-text'));
4871        });
4872    });
4873
4874}(jQuery));
4875
4876function gm_authFailure() {
4877    var warning = jQuery('.mptbm-map-warning').html();
4878    jQuery('#mptbm_map_area').html('<div class="mptbm-map-warning"><h6>' + warning + '</h6></div>');
4879}
4880// Utility: Detect iOS
4881function mptbm_is_ios() {
4882    return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
4883}
4884
4885// Utility: Detect Safari
4886function mptbm_is_safari() {
4887    return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
4888}
4889
4890// Fallback distance calculation for Safari when Google Maps API fails
4891function mptbm_fallback_distance_calculation(start_place, end_place) {
4892
4893    // Simple fallback: show placeholder values
4894    var fallback_distance = "Calculating...";
4895    var fallback_duration = "Calculating...";
4896
4897    // Update UI with fallback values
4898    jQuery(".mptbm_total_distance").html(fallback_distance);
4899    jQuery(".mptbm_total_time").html(fallback_duration);
4900    jQuery(".mptbm_distance_time").slideDown("fast");
4901
4902    // Set cookies with fallback values
4903    var now = new Date();
4904    var time = now.getTime();
4905    var expireTime = time + 3600 * 1000 * 12;
4906    now.setTime(expireTime);
4907
4908    var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax";
4909    document.cookie = "mptbm_distance=" + encodeURIComponent("0") + cookieOptions;
4910    document.cookie = "mptbm_distance_text=" + encodeURIComponent(fallback_distance) + cookieOptions;
4911    document.cookie = "mptbm_duration=" + encodeURIComponent("0") + cookieOptions;
4912    document.cookie = "mptbm_duration_text=" + encodeURIComponent(fallback_duration) + cookieOptions;
4913
4914    // Try to use server-side calculation as backup
4915    if (typeof mp_ajax_url !== 'undefined') {
4916        console.log("MPTBM JS: Calling server-side fallback");
4917        jQuery.ajax({
4918            type: "POST",
4919            url: mp_ajax_url,
4920            data: {
4921                action: "mptbm_calculate_distance_fallback",
4922                start_place: start_place,
4923                end_place: end_place
4924            },
4925            success: function (response) {
4926                console.log("MPTBM JS: Server fallback response", response);
4927                if (response.success && response.data) {
4928                    var distElem = jQuery(".mptbm_total_distance");
4929                    var timeElem = jQuery(".mptbm_total_time");
4930                    console.log("MPTBM JS: Updating UI. Dist Elem length:", distElem.length, "Time Elem length:", timeElem.length);
4931
4932                    distElem.html(response.data.distance_text);
4933                    timeElem.html(response.data.duration_text);
4934
4935                    // Update hidden inputs for AJAX fallback
4936                    var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area');
4937                    if (mapArea.length > 0) {
4938                        mapArea.find('input[name="mptbm_hidden_distance"]').val(response.data.distance);
4939                        mapArea.find('input[name="mptbm_hidden_duration"]').val(response.data.duration);
4940                        mapArea.find('input[name="mptbm_hidden_distance_text"]').val(response.data.distance_text);
4941                        mapArea.find('input[name="mptbm_hidden_duration_text"]').val(response.data.duration_text);
4942                    }
4943
4944                    // Update cookies with server response
4945                    document.cookie = "mptbm_distance=" + encodeURIComponent(response.data.distance) + cookieOptions;
4946                    document.cookie = "mptbm_distance_text=" + encodeURIComponent(response.data.distance_text) + cookieOptions;
4947                    document.cookie = "mptbm_duration=" + encodeURIComponent(response.data.duration) + cookieOptions;
4948                    document.cookie = "mptbm_duration_text=" + encodeURIComponent(response.data.duration_text) + cookieOptions;
4949                }
4950            },
4951            error: function () {
4952                console.log("Server-side distance calculation also failed");
4953            }
4954        });
4955    }
4956}
4957
4958// "Best Price" badge on the search-results vehicle cards: computed from the
4959// actual prices rendered for the current search (not a fabricated claim).
4960// The vehicle list loads in over AJAX after a search, and .mainSection
4961// itself doesn't exist until then, so this watches the whole document for
4962// it to appear/change rather than trying to bind to a specific element.
4963(function ($) {
4964    "use strict";
4965
4966    function highlightBestPrice() {
4967        $('.mainSection').each(function () {
4968            var mainSection = $(this);
4969            var cheapestBtn = null;
4970            var cheapestPrice = Infinity;
4971
4972            mainSection.find('.mptbm_transport_select[data-transport-price]').each(function () {
4973                var price = parseFloat($(this).attr('data-transport-price'));
4974                var item = $(this).closest('.mptbm_booking_item');
4975                if (!price || price <= 0 || item.hasClass('mptbm_booking_item_hidden')) {
4976                    return;
4977                }
4978                if (price < cheapestPrice) {
4979                    cheapestPrice = price;
4980                    cheapestBtn = $(this);
4981                }
4982            });
4983
4984            mainSection.find('.mptbm_best_price_badge').remove();
4985
4986            if (cheapestBtn) {
4987                var image = cheapestBtn.closest('.mptbm_booking_item').find('.mptbm_vehicle_image').first();
4988                image.append('<span class="mptbm_best_price_badge">Best Price</span>');
4989            }
4990        });
4991    }
4992
4993    // Results toolbar: live count of vehicles actually visible after geo-fence /
4994    // availability filtering (real data — same "hidden" class the rest of the
4995    // page already relies on), not the raw server-side query count.
4996    function updateResultsCount() {
4997        $('.mainSection').each(function () {
4998            var mainSection = $(this);
4999            var visible = mainSection.find('.mptbm_booking_item').not('.mptbm_booking_item_hidden').length;
5000            mainSection.find('.mptbm_results_count_number').text(visible);
5001        });
5002    }
5003
5004    var debounceTimer = null;
5005    function scheduleHighlight() {
5006        clearTimeout(debounceTimer);
5007        debounceTimer = setTimeout(function () {
5008            highlightBestPrice();
5009            updateResultsCount();
5010        }, 150);
5011    }
5012
5013    // Sort vehicle cards by their real rendered price. "Recommended" restores
5014    // the original server-rendered order (first time we see a given results
5015    // list, its current DOM order is cached as that baseline).
5016    function sortVehicles(mainSection, mode) {
5017        var area = mainSection.find('.mp_sticky_depend_area').first();
5018        var wrappers = area.children('.mptbm-vehicle-wrapper').toArray();
5019        if (!wrappers.length) {
5020            return;
5021        }
5022        if (!area.data('mptbmOriginalOrder')) {
5023            area.data('mptbmOriginalOrder', wrappers.slice());
5024        }
5025
5026        var ordered;
5027        if (mode === 'price_low' || mode === 'price_high') {
5028            ordered = wrappers.slice().sort(function (a, b) {
5029                var priceA = parseFloat($(a).find('[data-transport-price]').first().attr('data-transport-price')) || 0;
5030                var priceB = parseFloat($(b).find('[data-transport-price]').first().attr('data-transport-price')) || 0;
5031                return mode === 'price_high' ? priceB - priceA : priceA - priceB;
5032            });
5033        } else if (mode === 'rating') {
5034            ordered = wrappers.slice().sort(function (a, b) {
5035                var ratingA = parseFloat($(a).find('[data-transport-rating]').first().attr('data-transport-rating')) || 0;
5036                var ratingB = parseFloat($(b).find('[data-transport-rating]').first().attr('data-transport-rating')) || 0;
5037                return ratingB - ratingA;
5038            });
5039        } else {
5040            ordered = area.data('mptbmOriginalOrder');
5041        }
5042
5043        var anchor = area.find('.geo-fence-no-transport').first();
5044        ordered.forEach(function (el) {
5045            if (anchor.length) {
5046                $(el).insertBefore(anchor);
5047            } else {
5048                area.append(el);
5049            }
5050        });
5051    }
5052
5053    $(document).ready(function () {
5054        highlightBestPrice();
5055        updateResultsCount();
5056        if (typeof MutationObserver !== 'undefined') {
5057            new MutationObserver(scheduleHighlight).observe(document.body, { childList: true, subtree: true });
5058        }
5059    });
5060
5061    $(document).on('change', '.mptbm_sort_select', function () {
5062        var mainSection = $(this).closest('.mainSection');
5063        sortVehicles(mainSection, $(this).val());
5064    });
5065
5066    // Grid/List view toggle - purely a layout reflow of the same real cards,
5067    // no data changes.
5068    $(document).on('click', '.mptbm_results_view_toggle button', function () {
5069        var $btn = $(this);
5070        if ($btn.hasClass('is-active')) {
5071            return;
5072        }
5073        var view = $btn.data('view');
5074        var area = $btn.closest('.mainSection').find('.mp_sticky_depend_area').first();
5075        $btn.siblings().removeClass('is-active');
5076        $btn.addClass('is-active');
5077        area.toggleClass('mptbm_view_grid', view === 'grid');
5078        area.toggleClass('mptbm_view_list', view === 'list');
5079    });
5080})(jQuery);
5081
5082// Multi-row "Add Extra Stop" (.mptbm_extra_stops_wrapper in get_details.php).
5083// This markup had no JS behind it at all - clicking "Add Extra Stop" did
5084// nothing. Kept self-contained (own OSM search, not the shared
5085// mptbm_setup_osm_autocomplete/mptbm_handle_osm_address_selection pair) since
5086// those are hard-wired to the single start/end/legacy-extra marker slots and
5087// reworking them for an arbitrary number of rows risked regressing the
5088// pickup/dropoff autocomplete that already works.
5089(function ($) {
5090    "use strict";
5091
5092    // Both mptbm_set_cookie_distance_duration (Google) and
5093    // mptbm_calculate_osm_distance (OSM) now read every current
5094    // .mptbm_extra_stop_place_input / .mptbm_extra_stop_coords fresh from the
5095    // DOM on each call - this just needs to trigger the right one whenever a
5096    // stop is picked or removed, same as the existing start/end change handlers do.
5097    function mptbmExtraStopTriggerRecalculation() {
5098        var mapTypeEl = document.getElementById('mptbm_map_type');
5099        if (mapTypeEl && mapTypeEl.value === 'openstreetmap') {
5100            if (typeof mptbm_calculate_osm_distance === 'function') {
5101                mptbm_calculate_osm_distance();
5102            }
5103            return;
5104        }
5105        var startInput = document.getElementById('mptbm_map_start_place') || document.getElementById('mptbm_manual_start_place');
5106        var endInput = document.getElementById('mptbm_map_end_place') || document.getElementById('mptbm_manual_end_place');
5107        if (typeof mptbm_set_cookie_distance_duration === 'function') {
5108            mptbm_set_cookie_distance_duration(startInput ? startInput.value : '', endInput ? endInput.value : '');
5109        }
5110    }
5111
5112    function mptbmExtraStopMaxRows($wrapper) {
5113        var max = parseInt($wrapper.attr('data-max-stops'), 10);
5114        return max > 0 ? max : 3;
5115    }
5116
5117    function mptbmExtraStopToggleAddLink($wrapper) {
5118        var max = mptbmExtraStopMaxRows($wrapper);
5119        var count = $wrapper.find('.mptbm_extra_stops_list').children('.mptbm_extra_stop_row').length;
5120        $wrapper.find('.mptbm_add_extra_stop_row').toggle(count < max);
5121    }
5122
5123    function mptbmExtraStopSearch(query, container, input, coordsInput) {
5124        container.innerHTML = '<div style="padding: 9px 12px; color:#94a3b8; font-size:13px;">Searching&hellip;</div>';
5125        container.style.display = 'block';
5126
5127        var body = new URLSearchParams();
5128        body.append('action', 'mptbm_osm_search');
5129        body.append('nonce', mptbm_ajax.osm_nonce);
5130        body.append('q', query);
5131
5132        fetch(mptbm_ajax.ajax_url, {
5133            method: 'POST',
5134            headers: {
5135                'Content-Type': 'application/x-www-form-urlencoded',
5136                'X-Requested-With': 'XMLHttpRequest'
5137            },
5138            body: body,
5139            credentials: 'same-origin'
5140        })
5141            .then(function (response) { return response.json(); })
5142            .then(function (response) {
5143                // A slower-to-resolve older search (e.g. from a stop row the
5144                // customer has since edited or moved on from) must not pop
5145                // this dropdown back open with stale results out from under
5146                // whatever they're doing now - bail if the field no longer
5147                // holds the text this response was for.
5148                if (input.value.trim() !== query) {
5149                    return;
5150                }
5151
5152                container.innerHTML = '';
5153
5154                if (!response.success || !response.data || !response.data.length) {
5155                    container.innerHTML = '<div style="padding: 9px 12px; color:#94a3b8; font-size:13px;">No results found</div>';
5156                    container.style.display = 'block';
5157                    return;
5158                }
5159
5160                response.data.forEach(function (result) {
5161                    var item = document.createElement('div');
5162                    item.style.cssText = 'display:flex; align-items:flex-start; gap:10px; padding:10px 12px; margin:2px 0; cursor:pointer; border-radius:10px; color:#0f172a; font-size:13.5px; font-weight:500; line-height:1.4;';
5163
5164                    var icon = document.createElement('i');
5165                    icon.className = 'fas fa-map-marker-alt';
5166                    icon.style.cssText = 'flex:0 0 auto; width:14px; margin-top:2px; color:#94a3b8; font-size:13px;';
5167
5168                    var text = document.createElement('span');
5169                    text.textContent = result.display_name;
5170                    text.style.cssText = 'flex:1 1 auto; min-width:0;';
5171
5172                    item.appendChild(icon);
5173                    item.appendChild(text);
5174
5175                    item.addEventListener('click', function () {
5176                        input.value = result.display_name;
5177                        // Matches the "lat,lng" string format
5178                        // get_server_distance_with_stops() parses from
5179                        // mptbm_extra_stop_place_coordinates[].
5180                        coordsInput.value = parseFloat(result.lat) + ',' + parseFloat(result.lon);
5181                        container.style.display = 'none';
5182
5183                        // Drop/replace a pin for this specific row so the map
5184                        // shows the stop, not just the bent route line.
5185                        if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map && typeof L !== 'undefined') {
5186                            if (input._mptbmStopMarker) {
5187                                mptbm_osm_map.removeLayer(input._mptbmStopMarker);
5188                            }
5189                            input._mptbmStopMarker = L.marker([parseFloat(result.lat), parseFloat(result.lon)], {
5190                                title: result.display_name
5191                            }).addTo(mptbm_osm_map).bindPopup(result.display_name);
5192                        }
5193
5194                        mptbmExtraStopTriggerRecalculation();
5195                    });
5196                    item.addEventListener('mouseenter', function () { this.style.backgroundColor = '#f1f4f9'; });
5197                    item.addEventListener('mouseleave', function () { this.style.backgroundColor = ''; });
5198
5199                    container.appendChild(item);
5200                });
5201            })
5202            .catch(function () {
5203                if (input.value.trim() !== query) {
5204                    return;
5205                }
5206                container.innerHTML = '<div style="padding: 9px 12px; color:#dc2626; font-size:13px;">Search failed</div>';
5207            });
5208    }
5209
5210    function mptbmExtraStopSetupAutocomplete($row) {
5211        var input = $row.find('.mptbm_extra_stop_place_input')[0];
5212        var coordsInput = $row.find('.mptbm_extra_stop_coords')[0];
5213        if (!input || !coordsInput) {
5214            return;
5215        }
5216
5217        var mapTypeEl = document.getElementById('mptbm_map_type');
5218        var isOSM = !mapTypeEl || mapTypeEl.value === 'openstreetmap';
5219
5220        if (!isOSM) {
5221            if (typeof google === 'undefined' || !google.maps || !google.maps.places) {
5222                return;
5223            }
5224            var autocomplete = new google.maps.places.Autocomplete(input);
5225            var restrict = $('[name="mptbm_restrict_search_country"]').val();
5226            var country = $('[name="mptbm_country"]').val();
5227            if (restrict === 'yes' && country) {
5228                autocomplete.setComponentRestrictions({ country: [country] });
5229            }
5230            google.maps.event.addListener(autocomplete, 'place_changed', function () {
5231                var place = autocomplete.getPlace();
5232                if (place && place.geometry && place.geometry.location) {
5233                    coordsInput.value = place.geometry.location.lat() + ',' + place.geometry.location.lng();
5234                } else {
5235                    coordsInput.value = '';
5236                }
5237                mptbmExtraStopTriggerRecalculation();
5238            });
5239            return;
5240        }
5241
5242        var resultsContainer = document.createElement('div');
5243        resultsContainer.className = 'mptbm-osm-autocomplete';
5244        resultsContainer.style.cssText = 'position: fixed; box-sizing: border-box; font-size:14px; background: #fff; border: 1px solid #e7eaf0; border-radius: 14px; max-height: 240px; overflow-y: auto; z-index: 99999 !important; display: none; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06); padding: 6px;';
5245        document.body.appendChild(resultsContainer);
5246        // Appended to body (so it can float over everything), not to the row
5247        // itself - stash a reference so removing the row can clean this up
5248        // too, instead of leaving an orphaned dropdown behind forever.
5249        input._mptbmStopResultsContainer = resultsContainer;
5250
5251        function positionDropdown() {
5252            var rect = input.getBoundingClientRect();
5253            resultsContainer.style.top = (rect.bottom + 2) + 'px';
5254            resultsContainer.style.left = rect.left + 'px';
5255            resultsContainer.style.width = rect.width + 'px';
5256        }
5257
5258        var debounceTimer = null;
5259        input.addEventListener('input', function (e) {
5260            clearTimeout(debounceTimer);
5261            var query = e.target.value.trim();
5262            coordsInput.value = '';
5263            if (query.length < 3) {
5264                resultsContainer.style.display = 'none';
5265                return;
5266            }
5267            debounceTimer = setTimeout(function () {
5268                positionDropdown();
5269                mptbmExtraStopSearch(query, resultsContainer, input, coordsInput);
5270            }, 300);
5271        });
5272
5273        window.addEventListener('scroll', positionDropdown);
5274        window.addEventListener('resize', positionDropdown);
5275        document.addEventListener('click', function (e) {
5276            if (e.target !== input && !resultsContainer.contains(e.target)) {
5277                resultsContainer.style.display = 'none';
5278            }
5279        });
5280    }
5281
5282    $(document).on('click', '.mptbm_add_extra_stop_row', function () {
5283        var $wrapper = $(this).closest('.mptbm_extra_stops_wrapper');
5284        var $list = $wrapper.find('.mptbm_extra_stops_list');
5285
5286        if ($list.children('.mptbm_extra_stop_row').length >= mptbmExtraStopMaxRows($wrapper)) {
5287            return;
5288        }
5289
5290        var template = $wrapper.find('#mptbm_extra_stop_row_template')[0];
5291        if (!template || !template.content) {
5292            return;
5293        }
5294
5295        $list.append(template.content.cloneNode(true));
5296        mptbmExtraStopSetupAutocomplete($list.children('.mptbm_extra_stop_row').last());
5297        mptbmExtraStopToggleAddLink($wrapper);
5298    });
5299
5300    $(document).on('click', '.mptbm_remove_extra_stop_row', function () {
5301        var $wrapper = $(this).closest('.mptbm_extra_stops_wrapper');
5302        var $row = $(this).closest('.mptbm_extra_stop_row');
5303        var input = $row.find('.mptbm_extra_stop_place_input')[0];
5304        var hadCoords = !!$row.find('.mptbm_extra_stop_coords').val();
5305        if (input && input._mptbmStopMarker && typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) {
5306            mptbm_osm_map.removeLayer(input._mptbmStopMarker);
5307        }
5308        if (input && input._mptbmStopResultsContainer && input._mptbmStopResultsContainer.parentNode) {
5309            input._mptbmStopResultsContainer.parentNode.removeChild(input._mptbmStopResultsContainer);
5310        }
5311        $row.remove();
5312        mptbmExtraStopToggleAddLink($wrapper);
5313        if (hadCoords) {
5314            mptbmExtraStopTriggerRecalculation();
5315        }
5316    });
5317})(jQuery);

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.