PageSourceSearch

https://tiketi.com/wp-content/plugins/bus-booking-manager/js/mage_style.js?ver=1790968322

js tiketi.com collected 2026-10-02 19:12:32 UTC 47,716 bytes, 1,017 lines download raw bytes

1(function ($) {
2    "use strict";
3    $('#one_way').on('click', function () {
4        $('.mage_return_date').slideUp(300).removeClass('mage_hidden').find('input').val('');
5    });
6    $('#return').on('click', function () {
7        $('.mage_return_date').slideDown(300).removeClass('mage_hidden');
8    });
9    $('.mage_input_select .mage_route_list li').on('hover , click', function () {
10        let route = $(this).attr('data-route');
11        $(this).parents('.mage_input_select').find('input').val(route);
12    });
13    $('.mage_input_select input').on({
14        keyup: function () {
15            let input = $(this).val().toLowerCase();
16            $(this).siblings('ul.mage_route_list').find('li').filter(function () {
17                $(this).toggle($(this).attr('data-route').toLowerCase().indexOf(input) > -1);
18            });
19        },
20        blur: function () {
21            let input = $(this).val().toLowerCase();
22            let flag = 0;
23            $(this).siblings('ul.mage_route_list').find('li').filter(function () {
24                if ($(this).attr('data-route').toLowerCase() === input) {
25                    flag = 1;
26                }
27            });
28            if (flag < 1) {
29                $(this).val('');
30            }
31        },
32        click: function () {
33            $(this).siblings('ul.mage_route_list').find('li').slideDown(200);
34        }
35    });
36    /*
37     * Delegated from document so these keep working on markup that arrives
38     * after page load. Search results are rendered in place in the admin
39     * booking screen, and direct bindings were lost on every re-render:
40     * the quantity still changed (another script handles that) but the
41     * sub total was never recalculated, so it sat at 0.
42     */
43    $(document).on('keyup keydown', '.mage_form_group input.mage_form', function (e) {
44        if (e.keyCode === 13) {
45            e.preventDefault();
46            return false;
47        }
48        let target = $(this);
49        let value = parseInt(target.val());
50        mageTicketQty(target, value);
51    });
52    $(document).on('click', '.mage_qty_dec', function () {
53        let target = $(this).siblings('input');
54        let value = (parseInt(target.val()) - 1) > 0 ? (parseInt(target.val()) - 1) : 0;
55        target.trigger('input');
56        mageTicketQty(target, value);
57    });
58    $(document).on('click', '.mage_qty_inc', function () {
59        let target = $(this).siblings('input');
60        let value = target.val() ? parseInt(target.val()) + 1 : 1;
61        target.trigger('input');
62        mageTicketQty(target, value);
63    });
64
65    // Extra Service Price
66    $(document).on('change', '.extra-qty-box', function () {
67        const target = $(this);
68        let value = target.find('option:selected').val();
69        if (value == undefined) {
70            value = target.val();
71        }
72        mageExtServiceQty(target, value);
73    });
74    $(document).on('click', '.mage_es_qty_minus', function () {
75        const target = $(this).siblings('input');
76        const value = parseInt(target.val()) - 1;
77        target.trigger('input');
78        mageExtServiceQty(target, value);
79    });
80    $(document).on('click', '.mage_es_qty_plus', function () {
81        const target = $(this).siblings('input');
82        const value = parseInt(target.val()) + 1;
83        target.trigger('input');
84        mageExtServiceQty(target, value);
85    });
86    // Extra Service Price END
87
88    // Extra Bag price qty change
89    $(document).on('keyup keydown', '.mage_customer_info_area .mage_eb_form_qty .extra_bag_qty', function (e) {
90        if (e.keyCode === 13) {
91            e.preventDefault();
92            return false;
93        }
94        let target = $(this);
95        let value = parseInt(target.val());
96        mageExtBagQty(target, value);
97    });
98    $(document).on('click', '.mage_customer_info_area .mage_eb_form_qty .mage_eb_qty_minus', function () {
99        const target = $(this).siblings('input');
100        const value = parseInt(target.val()) - 1;
101        mageExtBagQty(target, value);
102    })
103    $(document).on('click', '.mage_customer_info_area .mage_eb_form_qty .mage_eb_qty_plus', function () {
104        const target = $(this).siblings('input');
105        const value = parseInt(target.val()) + 1;
106        mageExtBagQty(target, value);
107    })
108    // Extra Bag price qty change END
109
110    /*
111     * Delegated: the button is type="button" and does nothing on its own --
112     * this handler validates and then triggers the hidden submit. Bound
113     * directly it was lost whenever results were re-rendered in place, which
114     * left Book Now completely inert after an in-page search.
115     */
116    $(document).on('click', 'button.mage_book_now', function () {
117
118        $(this).parents('.mage_search_list').find('.mage-seat-available').hide();
119        let ticket = 0;
120        let currentTarget = $(this).parents('.mage_search_list').find('.mage_form[data-price]');
121        let seat_available = $(this).parents('.mage_search_list').attr('data-seat-available');
122        currentTarget.each(function (index) {
123            ticket += $(this).val() ? parseInt($(this).val()) : 0;
124        });
125        var pic_ele = $(this).parents('.mage_search_list').find('select[name="mage_pickpoint"]');
126        var pickpoint = $(this).parents('.mage_search_list').find('select[name="mage_pickpoint"] option:selected').val();
127
128        if (ticket > parseInt(seat_available)) {
129            $(this).parents('.mage_search_list').find('.mage-seat-available').show();
130            return false;
131        }
132        if (ticket > 0) {
133            // Pickup Point Validation
134            if (pic_ele.children().length > 0) {
135                if (pickpoint != '') { // Pass
136                    mageSubTotal(currentTarget);
137                    wbbmProceedToBook($(this));
138                } else { // Fail
139                    $(this).parents('.mage_search_list').find('select[name="mage_pickpoint"]').addClass('mage_error').trigger('focus');
140                }
141            } else {
142                mageSubTotal(currentTarget);
143                wbbmProceedToBook($(this));
144            }
145        } else {
146            currentTarget.addClass('mage_error').trigger('focus');
147        }
148    });
149
150    /*
151     * WooCommerce buses submit .single_add_to_cart_button immediately, same
152     * as always. Offline-payment buses instead open the confirmation modal
153     * (inc/clean/layout/book-now-area.php) -- only one of the two hidden
154     * submit buttons is ever rendered for a given bus, so which one exists
155     * decides which path runs. The modal's own "Confirm Booking" button is
156     * what actually triggers .wbbm-offline-book-btn.
157     */
158    function wbbmProceedToBook($bookNowBtn) {
159        var $searchList = $bookNowBtn.parents('.mage_search_list');
160
161        /*
162         * Return trips hand off here: on an outbound card the customer is
163         * moved to the return list to pick that leg instead of booking this
164         * one straight away. The hook lives in js/wbbm-search-modern.js and
165         * returns true only when it took over, so one-way searches and the
166         * return list itself fall through to the normal path below.
167         */
168        if (typeof window.wbbmAdvanceToReturnLeg === 'function' && window.wbbmAdvanceToReturnLeg($searchList)) {
169            return;
170        }
171
172        var $offlineBtn = $searchList.find('button.wbbm-offline-book-btn');
173
174        if ($offlineBtn.length) {
175            wbbmOpenOfflineModal($searchList);
176            return;
177        }
178
179        /*
180         * WooCommerce buses open the same drawer and add to the cart from
181         * there, so the customer sees what they are buying before the
182         * checkout loads. The hook lives in js/wbbm-search-modern.js and
183         * returns true only when it took over; without it, or without a
184         * drawer on this bus, the original submit still runs.
185         */
186        if (typeof window.wbbmOpenWcDrawer === 'function' && window.wbbmOpenWcDrawer($searchList)) {
187            return;
188        }
189
190        $bookNowBtn.siblings('button.single_add_to_cart_button').trigger('click');
191    }
192
193    // Mirrors mageSubTotal()'s three loops (seat price, extra service, extra
194    // bag) but returns a plain number instead of writing formatted currency
195    // text into the DOM -- the offline modal needs a real number to add tax
196    // on top of, not a pre-formatted "$50.00" string to re-parse.
197    function wbbmCalcSubtotal($searchList) {
198        var subtotal = 0;
199        $searchList.find('.mage_form[data-price]').each(function () {
200            var unitPrice = parseFloat($(this).attr('data-price')) || 0;
201            var qty = parseFloat($(this).val()) || 0;
202            if (unitPrice * qty > 0) { subtotal += unitPrice * qty; }
203        });
204        $searchList.find('.wbbm_extra_service_table tbody tr').each(function () {
205            var es = $(this).find('.extra-qty-box');
206            var unitPrice = parseFloat(es.attr('data-price')) || 0;
207            var qty = parseFloat(es.val()) || 0;
208            if (unitPrice * qty > 0) { subtotal += unitPrice * qty; }
209        });
210        $searchList.find('.mage_form_list').each(function () {
211            var extBag = $(this).find('.extra_bag_qty');
212            var unitPrice = parseFloat(extBag.attr('data-price')) || 0;
213            var qty = parseFloat(extBag.val()) || 0;
214            if (unitPrice * qty > 0) { subtotal += unitPrice * qty; }
215        });
216        return subtotal;
217    }
218
219    function wbbmOfflineFieldVal($searchList, name) {
220        var $el = $searchList.find('[name="' + name + '"]').first();
221        return $el.length ? $el.val() : '';
222    }
223
224    function wbbmResetOfflineModal($modal) {
225        // Back to the form state every time the modal opens, in case a
226        // previous booking's result (or error) is still showing from last
227        // time -- it should never carry over into a fresh attempt.
228        $modal.find('.mage_offline_modal_form').show();
229        $modal.find('.mage_offline_modal_result').hide().empty();
230        $modal.find('.mage_offline_modal_foot_form').show();
231        $modal.find('.mage_offline_modal_foot_result').hide();
232        $modal.find('.mage_offline_submit_error').hide().empty();
233        $modal.find('.mage_offline_input').removeClass('mage_error');
234        wbbmSyncGatewayUI($modal);
235    }
236
237    var wbbmConfirmBtnLabels = {
238        offline: 'Confirm Booking',
239        stripe: 'Pay with Card',
240        paypal: 'Pay with PayPal'
241    };
242
243    // One Stripe.js instance + one `card` Element for the whole page --
244    // Stripe Elements don't need to be recreated per booking attempt, they
245    // just get mounted into whichever modal's container is relevant right
246    // now (there's only ever one .mage_offline_modal open at a time) and
247    // moved (via unmount + mount) if a different modal opens later, e.g. on
248    // a search-results page with several bus cards. A `card` Element (not
249    // client_secret-bound) can be created before any PaymentIntent exists;
250    // the client_secret only gets used later, at confirmCardPayment() time.
251    var wbbmStripe = null;
252    var wbbmStripeCard = null;
253    var wbbmStripeMountedEl = null;
254
255    function wbbmGetStripeCard() {
256        if (wbbmStripeCard) { return wbbmStripeCard; }
257        if (typeof Stripe === 'undefined' || !window.WbbmStripePK) { return null; }
258        wbbmStripe = Stripe(window.WbbmStripePK);
259        wbbmStripeCard = wbbmStripe.elements().create('card', {
260            style: { base: { fontSize: '14px', color: '#1f2937' }, invalid: { color: '#ef4444' } }
261        });
262        wbbmStripeCard.on('change', function (event) {
263            // event.error / cleared text lives per-modal (a different modal
264            // might be showing a stale error from last time otherwise),
265            // found via the Element's own current mount point.
266            var $errBox = wbbmStripeMountedEl ? $(wbbmStripeMountedEl).siblings('.mage_stripe_card_errors') : $();
267            $errBox.text(event.error ? event.error.message : '');
268        });
269        return wbbmStripeCard;
270    }
271
272    function wbbmMountStripeCard($container) {
273        var card = wbbmGetStripeCard();
274        if (!card || !$container.length) { return; }
275        var node = $container.get(0);
276        if (wbbmStripeMountedEl === node) { return; }
277        card.mount(node);
278        wbbmStripeMountedEl = node;
279    }
280
281    // Reflects whichever gateway is currently selected (the radio picker,
282    // when there's an actual choice, or the single hidden input when there
283    // isn't) into the parts of the modal that depend on it: the Offline
284    // instructions paragraph, the Stripe card box, and the Confirm button's
285    // label. Called on open and on every picker change, never assumes a
286    // paint that already happened is still current.
287    function wbbmSyncGatewayUI($modal) {
288        var gateway = $modal.find('input[name="wbbm_selected_gateway"]:checked').val()
289            || $modal.find('input[name="wbbm_selected_gateway"]').val()
290            || 'offline';
291
292        $modal.find('.mage_offline_instructions').toggle(gateway === 'offline');
293        $modal.find('.mage_offline_confirm_btn').text(wbbmConfirmBtnLabels[gateway] || wbbmConfirmBtnLabels.offline);
294
295        var $stripeBox = $modal.find('.mage_stripe_card_box');
296        if ($stripeBox.length) {
297            $stripeBox.toggle(gateway === 'stripe');
298            if (gateway === 'stripe') {
299                wbbmMountStripeCard($modal.find('.mage_stripe_card_element'));
300            }
301        }
302
303        return gateway;
304    }
305
306    $(document).on('change', '.mage_offline_gateway_picker input[type="radio"]', function () {
307        var $picker = $(this).parents('.mage_offline_gateway_picker');
308        var $modal = $(this).parents('.mage_offline_modal');
309        $picker.attr('data-selected', $(this).val());
310        $picker.find('.mage_offline_gateway_card').removeClass('is-selected');
311        $(this).parents('.mage_offline_gateway_card').addClass('is-selected');
312        wbbmSyncGatewayUI($modal);
313    });
314
315    function wbbmOpenOfflineModal($searchList) {
316        var $modal = $searchList.find('.mage_offline_modal');
317        if (!$modal.length) { return; }
318
319        // Marked open BEFORE reset/sync below (which can mount the Stripe
320        // card Element into this modal) -- Stripe.js needs the container to
321        // actually have layout/dimensions at mount time, which it won't
322        // while the modal is still display:none.
323        $modal.addClass('is-open');
324
325        wbbmResetOfflineModal($modal);
326
327        var subtotal = wbbmCalcSubtotal($searchList);
328        var taxRate = parseFloat($searchList.find('.mage_offline_tax_rate').val()) || 0;
329        var taxAmount = subtotal * (taxRate / 100);
330        var total = subtotal + taxAmount;
331
332        var start = wbbmOfflineFieldVal($searchList, 'start_stops');
333        var end = wbbmOfflineFieldVal($searchList, 'end_stops');
334        var journeyDate = wbbmOfflineFieldVal($searchList, 'journey_date');
335        var startTime = wbbmOfflineFieldVal($searchList, 'user_start_time');
336
337        $modal.find('.mage_offline_summary_route_val').text(start && end ? (start + ' → ' + end) : '—');
338        $modal.find('.mage_offline_summary_date_val').text(journeyDate || '—');
339        $modal.find('.mage_offline_summary_time_val').text(startTime || '—');
340
341        var $rows = $modal.find('.mage_offline_summary_rows').empty();
342        $searchList.find('.mage_form[data-price]').each(function () {
343            var qty = parseInt($(this).val(), 10) || 0;
344            if (qty < 1) { return; }
345            var title = $(this).attr('data-ticket-title') || '';
346            var unitPrice = parseFloat($(this).attr('data-price')) || 0;
347            $rows.append(
348                '<div class="mage_offline_summary_row">' +
349                    '<span>' + title + ' x ' + qty + '</span>' +
350                    '<span>' + wbbm_woo_price_format(unitPrice * qty) + '</span>' +
351                '</div>'
352            );
353        });
354
355        $modal.find('.mage_offline_summary_subtotal_val').text(wbbm_woo_price_format(subtotal));
356        var $taxRow = $modal.find('.mage_offline_summary_tax');
357        if (taxRate > 0) {
358            $taxRow.show();
359            $modal.find('.mage_offline_summary_tax_label').text('Tax (' + taxRate + '%)');
360            $modal.find('.mage_offline_summary_tax_val').text(wbbm_woo_price_format(taxAmount));
361        } else {
362            $taxRow.hide();
363        }
364        $modal.find('.mage_offline_summary_total_val').text(wbbm_woo_price_format(total));
365    }
366
367    // The header "x" and the dedicated result-state "Close" button both
368    // just hide the modal -- neither one submits or resets anything, so a
369    // customer can reopen it (Book Now again) and pick up where the form
370    // last was, or see the same result again if they already booked.
371    $(document).on('click', '.mage_offline_modal_close, .mage_offline_modal_cancel, .mage_offline_modal_close_btn', function () {
372        $(this).parents('.mage_offline_modal').removeClass('is-open');
373    });
374    $(document).on('click', '.mage_offline_modal', function (e) {
375        if (e.target === this) { $(this).removeClass('is-open'); }
376    });
377    $(document).on('keydown', function (e) {
378        if (e.key === 'Escape') { $('.mage_offline_modal.is-open').removeClass('is-open'); }
379    });
380
381    var wbbmOfflineErrorMessages = {
382        invalid_nonce: 'Your session expired -- please close this and try again.',
383        invalid_bus: 'This bus could not be found.',
384        wrong_payment_mode: 'This bus is not set up for offline booking.',
385        missing_contact_info: 'Please fill in your name, phone and a valid email address.',
386        missing_route_fields: 'Please choose your boarding and dropping points and travel date.',
387        no_seats_selected: 'Please select at least one seat.',
388        sold_out: 'Sorry, not enough seats are available for this selection.',
389        insert_failed: 'Something went wrong recording your booking -- please try again.',
390        stripe_not_configured: 'Card payment is not available right now -- please choose another payment method.',
391        paypal_not_configured: 'PayPal is not available right now -- please choose another payment method.',
392        stripe_intent_failed: 'Could not start the card payment -- please try again.',
393        paypal_order_failed: 'Could not start the PayPal payment -- please try again.',
394        payment_not_completed: 'Payment could not be confirmed -- please try again, or use a different card.',
395        booking_not_found: 'We could not find that booking -- please try again.',
396        invalid_request: 'Something went wrong -- please try again.'
397    };
398
399    // Everything below goes into .html(), and the summary values come back
400    // from the server (bus name, stop names, the reference) -- run them
401    // through here so a stop called `<b>` renders as text instead of markup.
402    // Prices are deliberately NOT escaped: wbbm_woo_price_format() bakes in
403    // the WooCommerce currency symbol, which can legitimately be an HTML
404    // entity (`&pound;`), and escaping would print the entity source.
405    function wbbmEscHtml(value) {
406        return String(value === null || value === undefined ? '' : value)
407            .replace(/&/g, '&amp;')
408            .replace(/</g, '&lt;')
409            .replace(/>/g, '&gt;')
410            .replace(/"/g, '&quot;')
411            .replace(/'/g, '&#39;');
412    }
413
414    /** The rows describing one leg: which bus, which route, which day, how many seats. */
415    function wbbmLegRows(leg) {
416        var seatBits = [];
417        if (leg.entire) {
418            seatBits.push('Entire bus');
419        } else {
420            if (leg.adult) { seatBits.push(leg.adult + ' adult'); }
421            if (leg.child) { seatBits.push(leg.child + ' child'); }
422            if (leg.infant) { seatBits.push(leg.infant + ' infant'); }
423        }
424
425        return [
426            ['Bus', wbbmEscHtml(leg.bus_name)],
427            ['Route', wbbmEscHtml(leg.start) + ' <span class="wbbm-offline-route-arrow" aria-hidden="true">→</span> ' + wbbmEscHtml(leg.end)],
428            ['Journey date', wbbmEscHtml(leg.journey_date)],
429            ['Seats', wbbmEscHtml(seatBits.join(', '))]
430        ];
431    }
432
433    function wbbmRenderOfflineResult($modal, summary) {
434        /*
435         * A return trip comes back as two legs under one reference. A one-way
436         * booking has no `legs` key at all (and an older booking replayed
437         * through the Stripe/PayPal return trip may not either), so the
438         * summary itself stands in as the single leg.
439         */
440        var legs = (summary.legs && summary.legs.length) ? summary.legs : [summary];
441        var multiLeg = legs.length > 1;
442
443        var rows = [];
444        legs.forEach(function (leg, index) {
445            if (multiLeg) {
446                rows.push(['__heading__', index === 0 ? 'Outbound' : 'Return']);
447            }
448            rows = rows.concat(wbbmLegRows(leg));
449        });
450
451        if (multiLeg) {
452            rows.push(['__heading__', 'Payment']);
453        }
454        rows.push(['Subtotal', wbbm_woo_price_format(summary.subtotal)]);
455        if (parseFloat(summary.tax_amount) > 0) {
456            // No rate in the label on a return trip: each leg carries its own
457            // per-bus offline tax rate, so one percentage would be a lie.
458            rows.push([
459                multiLeg ? 'Tax' : ('Tax (' + wbbmEscHtml(summary.tax_rate) + '%)'),
460                wbbm_woo_price_format(summary.tax_amount)
461            ]);
462        }
463
464        // Offline is still "we'll follow up" (payment_status stays pending
465        // until the site owner confirms it manually); Stripe/PayPal have
466        // already actually charged/captured by the time this renders, so
467        // the wording should say so rather than implying anything is still
468        // owed or pending.
469        //
470        // Either way the *booking* itself is recorded, so the headline says
471        // so plainly and the pill underneath carries the honest payment
472        // state -- the customer should never have to read a paragraph to
473        // find out whether they got a seat.
474        var isOffline = ('offline' === summary.gateway || !summary.gateway);
475        var heading = isOffline ? 'Booking confirmed' : 'Payment confirmed';
476        var note = isOffline
477            ? 'Your seats are reserved. We\'ll contact you shortly to confirm payment.'
478            : 'Your booking is complete and your payment has gone through.';
479        var pillLabel = isOffline ? 'Payment pending' : 'Paid in full';
480        var pillState = isOffline ? 'is-pending' : 'is-paid';
481
482        if (multiLeg) {
483            note = isOffline
484                ? 'Both legs are reserved under one reference. We\'ll contact you shortly to confirm payment.'
485                : 'Both legs are booked under one reference and your payment has gone through.';
486        }
487
488        // The check draws itself in (see .wbbm-offline-success-mark in
489        // css/wbbm-search-modern.css); the whole block is announced at once
490        // via role="status" so a screen reader hears the outcome too, not
491        // just the sighted celebration.
492        var html = '<div class="wbbm-offline-success" role="status">' +
493            '<span class="wbbm-offline-success-mark" aria-hidden="true">' +
494                '<svg viewBox="0 0 52 52" focusable="false">' +
495                    '<circle class="wbbm-offline-success-mark-ring" cx="26" cy="26" r="24"/>' +
496                    '<path class="wbbm-offline-success-mark-tick" d="M15 27.5 L22.5 35 L37.5 19"/>' +
497                '</svg>' +
498            '</span>' +
499            '<h3 class="mage_offline_result_heading">' + heading + '</h3>' +
500            '<p class="wbbm-offline-success-note">' + note + '</p>' +
501            '<span class="wbbm-offline-success-pill ' + pillState + '">' + pillLabel + '</span>' +
502            '<span class="wbbm-offline-success-ref">' +
503                '<small>Booking reference</small>' +
504                '<strong>#' + wbbmEscHtml(summary.reference) + '</strong>' +
505            '</span>' +
506        '</div>' +
507            '<table class="wbbm-offline-booking-details">';
508        rows.forEach(function (row) {
509            if ('__heading__' === row[0]) {
510                html += '<tr class="wbbm-offline-leg-heading"><th colspan="2">' + row[1] + '</th></tr>';
511                return;
512            }
513            html += '<tr><th>' + row[0] + '</th><td>' + row[1] + '</td></tr>';
514        });
515        html += '<tr class="wbbm-offline-booking-total"><th>Total</th><td>' + wbbm_woo_price_format(summary.total_price) + '</td></tr>' +
516            '</table>';
517
518        $modal.find('.mage_offline_modal_result').html(html).show();
519        $modal.find('.mage_offline_modal_form').hide();
520        $modal.find('.mage_offline_modal_foot_form').hide();
521        $modal.find('.mage_offline_modal_foot_result').show();
522
523        // The held outbound leg has been recorded now. Forgetting it stops a
524        // later booking from silently re-posting the leg it already sold.
525        if (typeof window.wbbmClearHeldOutboundLeg === 'function') {
526            window.wbbmClearHeldOutboundLeg();
527        }
528    }
529
530    $(document).on('click', '.mage_offline_confirm_btn', function () {
531        var $btn = $(this);
532        var $modal = $btn.parents('.mage_offline_modal');
533        var $searchList = $modal.parents('.mage_search_list');
534        var $fields = $modal.find('.mage_offline_input');
535        var $errorMsg = $modal.find('.mage_offline_submit_error').hide().empty();
536        var valid = true;
537
538        $fields.each(function () {
539            if (!$(this).val() || !$(this).val().trim()) {
540                $(this).addClass('mage_error');
541                valid = false;
542            } else {
543                $(this).removeClass('mage_error');
544            }
545        });
546
547        if (!valid) { return; }
548
549        $modal.find('.mage_offline_input[name="wbbm_offline_email"]').each(function () {
550            // Same basic shape check the "email" input type already nudges
551            // toward -- a real is_email() check still happens server-side
552            // in inc/wbbm-offline-booking.php regardless.
553            if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($(this).val().trim())) {
554                $(this).addClass('mage_error');
555                valid = false;
556            }
557        });
558
559        if (!valid) { return; }
560
561        var gateway = wbbmSyncGatewayUI($modal);
562
563        if ('stripe' === gateway && !wbbmGetStripeCard()) {
564            $errorMsg.text('Card payment could not load -- please refresh and try again.').show();
565            return;
566        }
567
568        var originalText = $btn.text();
569        $btn.prop('disabled', true).text(gateway === 'paypal' ? 'Redirecting…' : 'Processing…');
570
571        function resetButton() { $btn.prop('disabled', false).text(originalText); }
572
573        /*
574         * On a return trip the server says WHICH leg failed. Naming it matters:
575         * "sold out" on a two-bus booking leaves the customer with no idea
576         * which leg to change. Nothing was recorded either way -- the legs are
577         * validated together and inserted all-or-nothing.
578         */
579        function showError(code, leg) {
580            resetButton();
581
582            var text = wbbmOfflineErrorMessages[code] || 'Something went wrong -- please try again.';
583
584            if (postData.legs && typeof leg === 'number') {
585                text = (0 === leg ? 'Outbound leg: ' : 'Return leg: ') + text
586                    + ' Nothing has been booked.';
587            }
588
589            $errorMsg.text(text).show();
590        }
591
592        // The same field set feeds all three gateways -- Offline validates
593        // and records in one step (inc/wbbm-offline-booking.php); Stripe
594        // validates + records as pending, then hands back a PaymentIntent
595        // client_secret to confirm right here with Stripe.js; PayPal
596        // validates + records as pending, then hands back a hosted approval
597        // URL to redirect to (inc/wbbm-custom-gateway-checkout.php).
598        var postData = {
599            action: 'wbbm_offline_book_now',
600            wbbm_offline_book_now: 1,
601            wbbm_offline_book_now_nonce: $searchList.find('input[name="wbbm_offline_book_now_nonce"]').val(),
602            bus_id: wbbmOfflineFieldVal($searchList, 'bus_id'),
603            journey_date: wbbmOfflineFieldVal($searchList, 'journey_date'),
604            start_stops: wbbmOfflineFieldVal($searchList, 'start_stops'),
605            end_stops: wbbmOfflineFieldVal($searchList, 'end_stops'),
606            user_start_time: wbbmOfflineFieldVal($searchList, 'user_start_time'),
607            bus_start_time: wbbmOfflineFieldVal($searchList, 'bus_start_time'),
608            mage_pickpoint: $searchList.find('select[name="mage_pickpoint"]').val(),
609            adult_quantity: $searchList.find('[name="adult_quantity"]').val(),
610            child_quantity: $searchList.find('[name="child_quantity"]').val(),
611            infant_quantity: $searchList.find('[name="infant_quantity"]').val(),
612            entire_quantity: $searchList.find('[name="entire_quantity"]').is(':checked') ? 1 : 0,
613            wbbm_offline_name: $modal.find('[name="wbbm_offline_name"]').val(),
614            wbbm_offline_phone: $modal.find('[name="wbbm_offline_phone"]').val(),
615            wbbm_offline_email: $modal.find('[name="wbbm_offline_email"]').val()
616        };
617
618        /*
619         * Return trips book both legs in one request. The outbound leg was
620         * held back when the customer picked it (wbbmAdvanceToReturnLeg in
621         * js/wbbm-search-modern.js); it goes first in `legs`, with the leg
622         * being confirmed here second, and the server records both under one
623         * order id or neither.
624         *
625         * The flat per-leg fields above stay on the request regardless: the
626         * server falls back to them when there is no `legs` key, which is
627         * what a one-way booking and the no-JS POST-back both send.
628         */
629        var heldOutbound = (typeof window.wbbmHeldOutboundLeg === 'function')
630            ? window.wbbmHeldOutboundLeg($searchList)
631            : null;
632
633        if (heldOutbound) {
634            postData.legs = [heldOutbound, {
635                bus_id: postData.bus_id,
636                journey_date: postData.journey_date,
637                start_stops: postData.start_stops,
638                end_stops: postData.end_stops,
639                user_start_time: postData.user_start_time,
640                bus_start_time: postData.bus_start_time,
641                mage_pickpoint: postData.mage_pickpoint,
642                adult_quantity: postData.adult_quantity,
643                child_quantity: postData.child_quantity,
644                infant_quantity: postData.infant_quantity,
645                entire_quantity: postData.entire_quantity
646            }];
647        }
648
649        if ('stripe' === gateway) {
650            postData.action = 'wbbm_create_stripe_payment_intent';
651        } else if ('paypal' === gateway) {
652            postData.action = 'wbbm_create_paypal_order';
653        }
654
655        $.ajax({
656            url: WbbmAjax.ajax_url,
657            type: 'POST',
658            data: postData,
659            dataType: 'json'
660        }).done(function (response) {
661            if (!response || !response.success) {
662                showError(
663                    response && response.data && response.data.code,
664                    response && response.data ? response.data.leg : undefined
665                );
666                return;
667            }
668
669            if ('offline' === gateway) {
670                resetButton();
671                wbbmRenderOfflineResult($modal, response.data);
672                return;
673            }
674
675            if ('paypal' === gateway) {
676                // The browser leaves the site here, same as any real
677                // checkout -- the button stays disabled ("Redirecting…")
678                // since there's nothing left to do on this page before the
679                // navigation happens.
680                window.location.href = response.data.redirect;
681                return;
682            }
683
684            // Stripe: confirm the card right here -- this is the one call
685            // that can prompt the customer for 3-D-Secure (Stripe.js shows
686            // that itself, inline, as part of this same promise) and is the
687            // only place actual card details are involved, handled entirely
688            // by Stripe.js/Elements and sent straight to Stripe.
689            wbbmStripe.confirmCardPayment(response.data.client_secret, {
690                payment_method: {
691                    card: wbbmStripeCard,
692                    billing_details: {
693                        name: postData.wbbm_offline_name,
694                        email: postData.wbbm_offline_email,
695                        phone: postData.wbbm_offline_phone
696                    }
697                }
698            }).then(function (result) {
699                if (result.error) {
700                    showError(null);
701                    $errorMsg.text(result.error.message || 'Card payment failed -- please try again.').show();
702                    return;
703                }
704                if (!result.paymentIntent || 'succeeded' !== result.paymentIntent.status) {
705                    showError('payment_not_completed');
706                    return;
707                }
708
709                // Never trust confirmCardPayment()'s client-side result
710                // alone -- re-verify server-side before showing this as a
711                // real confirmed booking.
712                $.ajax({
713                    url: WbbmAjax.ajax_url,
714                    type: 'POST',
715                    data: {
716                        action: 'wbbm_confirm_stripe_payment',
717                        reference: response.data.reference,
718                        payment_intent_id: result.paymentIntent.id
719                    },
720                    dataType: 'json'
721                }).done(function (confirmRes) {
722                    resetButton();
723                    if (confirmRes && confirmRes.success) {
724                        wbbmRenderOfflineResult($modal, confirmRes.data);
725                    } else {
726                        showError(confirmRes && confirmRes.data && confirmRes.data.code);
727                    }
728                }).fail(function () {
729                    showError(null);
730                    $errorMsg.text('Payment went through, but we could not confirm it here -- please contact us with your card statement.').show();
731                });
732            });
733        }).fail(function () {
734            resetButton();
735            $errorMsg.text('Connection error -- please try again.').show();
736        });
737    });
738
739    // Search Form Actions
740    $(document).on('click', function (event) {
741        let selectUl = $('.mage_input_select_list');
742        if (!$(event.target).parents().hasClass('mage_input_select') && selectUl.is(':visible')) {
743            let target = $('.mage_input_select input');
744            target.each(function (index) {
745                let input = $(this).val().toLowerCase();
746                let flag = 0;
747                $(this).parents('.mage_input_select').find('li').filter(function () {
748                    if ($(this).attr('data-route').toLowerCase() === input) {
749                        flag = 1;
750                        mage_bus_dropping_point(selectUl);
751                    }
752                });
753                if (flag < 1) {
754                    $(this).val('');
755                }
756            });
757            selectUl.slideUp(200);
758        }
759    });
760    $(document).on({
761        keyup: function () {
762            console.log('kkkdk')
763            let input = $(this).val().toLowerCase();
764            $(this).parents('.mage_input_select').find('.mage_input_select_list').find('li').filter(function () {
765                $(this).toggle($(this).attr('data-route').toLowerCase().indexOf(input) > -1);
766            });
767        },
768        click: function () {
769            $(this).parents('.mage_input_select').find('.mage_input_select_list').slideDown(200);
770            $(this).parents('.mage_input_select').find('.mage_input_select_list_static').slideDown(200);
771            $(this).parents('.route-input-wrap').addClass('activeMageSelect');
772            if ($(this).parents('.mage_input_select').hasClass('mage_bus_boarding_point')) {
773                if ($('.mage_bus_dropping_point').find('.route-input-wrap').hasClass('activeMageSelect')) {
774                    $('.mage_bus_dropping_point').find('.route-input-wrap').removeClass('activeMageSelect');
775                    $('.mage_bus_dropping_point').find('.mage_input_select_list').slideUp(200);
776                    $('.mage_bus_dropping_point').find('.mage_input_select_list_static').slide
776Up(200);
777                }
778            }
779        },
780        blur: function () {
781            $(this).parents('.route-input-wrap').removeClass('activeMageSelect');
782        }
783    }, '.mage_input_select input');
784
785    $(document).on({
786        click: function () {
787            let route = $(this).attr('data-route');
788            $(this).parents('.mage_input_select_list').slideUp(200).parents('.mage_input_select').find('input').val(route);
789            mage_bus_dropping_point($(this));
790            $(this).parents('.mage_input_select_list').siblings('.route-input-wrap').removeClass('activeMageSelect');
791        }
792    }, '.mage_input_select_list li');
793
794    $(document).on({
795        click: function () {
796            let route = $(this).attr('data-route');
797            $(this).parents('.mage_input_select_list_static').slideUp(200).parents('.mage_input_select').find('input').val(route);
798            mage_bus_dropping_point($(this));
799            $(this).parents('.mage_input_select_list_static').siblings('.route-input-wrap').removeClass('activeMageSelect');
800        }
801    }, '.mage_input_select_list_static li');
802
803    // Minimul Design Script
804    $(document).on({
805        click: function () {
806            var $this = $(this);
807            $this.parents('.mage-search-brief-row').toggleClass('opened');
808            $this.parents('.mage-search-brief-row').siblings('.mage-bus-booking-wrapper').slideToggle('fast');
809        }
810    }, '.mage-bus-detail-action');
811    // Minimul Design Script END
812
813    $(document).ready(function () {
814        $('.wbbm_entire_switch_wrapper #wbbm_entire_bus').click(function (e) {
815            const $this = $(this);
816            const priceInfoEl = $this.parents('.mage_search_list');
817            const price = priceInfoEl.find('.wbbm_entire_switch_wrapper').attr('data-entire-price')
818            if ($(this)[0].hasAttribute('checked')) {
819                $(this).attr('checked', false);
820                $(this).val('0');
821                $('input[name=adult_quantity]').closest('.mage_center_space').show();
822                $('input[name=child_quantity]').closest('.mage_center_space').show();
823                $('input[name=infant_quantity]').closest('.mage_center_space').show();
824                $('div.entire').hide();
825            }
826            else {
827                $(this).attr('checked', true);
828                $(this).val('1');
829                $('input[name=adult_quantity]').closest('.mage_center_space').hide();
830                $('input[name=child_quantity]').closest('.mage_center_space').hide();
831                $('input[name=infant_quantity]').closest('.mage_center_space').hide();
832
833                priceInfoEl.find('.mage_seat_qty').val(0);
834                const infoArea = priceInfoEl.find('.mage_customer_info_area');
835                infoArea.find('.adult').empty().hide();
836                infoArea.find('.child').empty().hide();
837                infoArea.find('.infant').empty().hide();
838                let passenger_info_title = $('#wbbm_entire_bus').attr('data-ticket-title');
839
840                let passenger_info_form = $('.mage_hidden_customer_info_form').html();
841                $('div.entire').html(passenger_info_form);
842                let passenger_info_user_type = $('div.entire .mage_form_list input[name="wbbm_user_type[]"]');
843                $('div.entire .mage_form_list .mage_form_list_title h4').html(passenger_info_title);
844                $(passenger_info_user_type).val('entire');
845                $('div.entire .mage_form_list').show();
846                $('div.entire').show();
847            }
848            mageSubTotal($(this));
849
850        });
851    })
852
853    function mage_bus_dropping_point(target) {
854        if (target.parents().hasClass('mage_bus_boarding_point')) {
855            var boarding_point = target.attr('data-route');
856
857            if (boarding_point != undefined) {
858                $.ajax({
859                    type: 'POST',
860                    // url: wbtm_ajax.wbtm_ajaxurl,
861                    url: WbbmAjax.ajax_url,
862                    data: { "action": "wbbm_load_dropping_point", "boarding_point": boarding_point, nonce: WbbmAjax.nonce },
863                    beforeSend: function () {
864                        $('#bus_end_route').val('');
865                        $('#wbtm_dropping_point_list').slideUp(200);
866                        $('#wbtm_show_msg').show();
867                        $('#wbtm_show_msg').html('<span>Loading..</span>');
868                    },
869                    success: function (data) {
870                        $('#wbtm_show_msg').hide();
871                        $('#bus_end_route').val('');
872                        $('.mage_bus_dropping_point .mage_input_select_list ul').html(data);
873                        // $('#wbtm_dropping_point_list').slideDown(200);
874                        $('#bus_end_route').trigger('click');
875                        $('#wbtm_dropping_point_list').siblings('.route-input-wrap').addClass('activeMageSelect');
876                    }
877                });
878                return false;
879            }
880        }
881    }
882
883    function mageExtServiceQty(target, value) {
884        let minSeat = parseInt(target.attr('min'));
885        let maxSeat = parseInt(target.attr('max'));
886        target.siblings('.mage_qty_inc , .mage_qty_dec').removeClass('mage_disabled');
887        if (value < minSeat || isNaN(value) || value === 0) {
888            value = minSeat;
889            target.siblings('.mage_qty_dec').addClass('mage_disabled');
890        }
891        if (value > maxSeat) {
892            value = maxSeat;
893            target.siblings('.mage_es_qty_plus').addClass('mage_disabled');
894        }
895        target.val(value);
896        mageError(value, target);
897        mageSubTotal(target);
898    }
899
900    function mageTicketQty(target, value) {
901        let minSeat = parseInt(target.attr('min'));
902        let maxSeat = get_max_qty(target);
903        target.siblings('.mage_qty_inc , .mage_qty_dec').removeClass('mage_disabled');
904        if (value < minSeat || isNaN(value) || value === 0) {
905            value = minSeat;
906            target.siblings('.mage_qty_dec').addClass('mage_disabled');
907        }
908        if (value > maxSeat) {
909            value = maxSeat;
910            target.siblings('.mage_qty_inc').addClass('mage_disabled');
911        }
912        target.val(value);
913        mageError(value, target);
914        mageSubTotal(target);
915    }
916    function get_max_qty(target) {
917        let available_quantity = parseInt($('[name="available_quantity"]').val()) || 0;
918        let adult_quantity = parseInt($('[name="adult_quantity"]').val()) || 0;
919        let child_quantity = parseInt($('[name="child_quantity"]').val()) || 0;
920        let infant_quantity = parseInt($('[name="infant_quantity"]').val()) || 0;
921        return available_quantity - (adult_quantity + child_quantity + infant_quantity) + (parseInt(target.val()) || 0);
922    }
923
924    function mageExtBagQty(target, value) {
925        let minSeat = parseInt(target.attr('min'));
926        let maxSeat = parseInt(target.attr('max'));
927        target.siblings('.mage_qty_inc , .mage_qty_dec').removeClass('mage_disabled');
928        if (value < minSeat || isNaN(value) || value === 0) {
929            value = minSeat;
930            target.siblings('.mage_eb_qty_minus').addClass('mage_disabled');
931        }
932        if (value > maxSeat) {
933            value = maxSeat;
934            target.siblings('.mage_eb_qty_plus').addClass('mage_disabled');
935            return;
936        }
937        target.val(value);
938        const perExtBagPrice = target.attr('data-price');
939        mageError(value, target);
940        mageSubTotal(target)
941    }
942
943    function mageError(value, target) {
944        if (value > 0) {
945            target.removeClass('mage_error');
946        } else {
947            target.addClass('mage_error').trigger('focus');
948        }
949    }
950    function mageSubTotal(target) {
951
952        let currentTarget = target.parents('.mage_search_list');
953        let subTotal = 0;
954        currentTarget.find('.mage_form[data-price]').each(function (index) {
955            let unitPrice = parseFloat($(this).attr('data-price'));
956            let ticket = parseFloat($(this).val());
957            subTotal = subTotal + (unitPrice * ticket > 0 ? unitPrice * ticket : 0);
958        });
959
960        // Extra Service
961        currentTarget.find('.wbbm_extra_service_table tbody tr').each(function () {
962            const es = $(this).find('.extra-qty-box');
963            const esUnitPrice = parseFloat(es.attr('data-price'));
964            const esQty = parseFloat(es.val());
965            subTotal = subTotal + (esUnitPrice * esQty > 0 ? esUnitPrice * esQty : 0);
966        });
967        // Extra Service END
968
969        // Ext Bag Price
970        currentTarget.find('.mage_form_list').each(function () {
971            const extBag = $(this).find('.extra_bag_qty');
972            const extBagUnitPrice = parseFloat(extBag.attr('data-price'));
973            const extQty = parseFloat(extBag.val());
974            subTotal = subTotal + (extBagUnitPrice * extQty > 0 ? extBagUnitPrice * extQty : 0);
975        })
976        // Ext Bag Price END
977        currentTarget.find('.mage_sub_total span').html(wbbm_woo_price_format(subTotal));
978        mageCustomerInfoForm(currentTarget);
979    }
980
981    function mageCustomerInfoForm(target) {
982        if (target.children().hasClass('mage_hidden_customer_info_form')) {
983            target.find('.mage_form[data-price]').each(function (index) {
984                let mageTicketType = $(this).attr('data-ticket-type');
985                let mageTargetClass = '.' + mageTicketType;
986                let currentTarget = target.find(mageTargetClass);
987                let ticketQTy = parseInt($(this).val());
988                if (ticketQTy < 1) {
989                    currentTarget.empty().slideUp(500);
990                } else {
991                    let currentFormLength = currentTarget.find('.mage_form_list').length;
992                    if (currentFormLength < ticketQTy) {
993                        let mageTicketTitle = $(this).attr('data-ticket-title');
994                        for (let i = currentFormLength; i < ticketQTy; i++) {
995                            target.find('.mage_hidden_customer_info_form h4').html(mageTicketTitle + (i + 1));
996                            target.find('.mage_hidden_customer_info_form input[name="wbbm_user_t
996ype[]"]').val(mageTicketType);
997                            let mageFormInfo = target.find('.mage_hidden_customer_info_form').html();
998                            currentTarget.append(mageFormInfo).slideDown('fast').find('.mage_form_list').slideDown(500);
999                        }
1000                    }
1001                    if (currentFormLength > ticketQTy) {
1002
1003                        while (currentFormLength > ticketQTy) {
1004                            currentTarget.find('.mage_form_list:last-child').slideUp(500).remove();
1005                            currentFormLength = currentTarget.find('.mage_form_list').length;
1006                        }
1007                    }
1008                }
1009            });
1010        }
1011        else {
1012            target.find('.mage_customer_info_area .child').html('<input type="hidden" name="custom_reg_user" value="no" />');
1013        }
1014    }
1015
1016
1017}(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.