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 (`£`), and escaping would print the entity source. 405 function wbbmEscHtml(value) { 406 return String(value === null || value === undefined ? '' : value) 407 .replace(/&/g, '&') 408 .replace(/</g, '<') 409 .replace(/>/g, '>') 410 .replace(/"/g, '"') 411 .replace(/'/g, '''); 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.