PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / js / booking.js

booking.js in Yatra – Travel Booking & Tour Operator Software 3.0.2.7, at assets/js/booking.js

1,671 lines 68.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Booking Page JavaScript
3 *
4 * Uses FormData API to automatically collect all form fields
5 * and submit via REST API
6 *
7 * @package Yatra
8 */
9
10 (function($) {
11 'use strict';
12
13 // API configuration - available throughout the module
14 const apiUrl = window.yatraBookingData?.apiUrl || '/wp-json/yatra/v1';
15 const nonce = window.yatraBookingData?.nonce || '';
16
17 // Unified REST base resolver with plain-permalink support
18 const getRestBase = () => {
19 const siteUrl = window.yatraBookingData?.siteUrl || window.location.origin || '';
20 let base =
21 window.yatraBookingData?.apiUrl ||
22 window.yatraBookingData?.restUrl ||
23 (window.wpApiSettings && window.wpApiSettings.root) ||
24 `${siteUrl.replace(/\/$/, '')}/wp-json`;
25 base = base.replace(/\/$/, '');
26 const permalinkStructure =
27 window.yatraBookingData?.permalinkStructure ||
28 window.yatraTripData?.permalinkStructure ||
29 window.yatraAdmin?.permalinkStructure ||
30 '';
31 // Default to plain when structure is unknown to avoid 404s under Plain permalinks
32 const isPlain = permalinkStructure === 'plain' || !permalinkStructure;
33 return { base, isPlain };
34 };
35
36 $(document).ready(function() {
37 const $form = $('#yatra-booking-form');
38 const $submitBtn = $('#yatra-submit-booking');
39 const isRemainingPayment = Boolean(window.yatraBookingData?.isRemainingPayment) || ($form.data('is-remaining-payment') === 'yes');
40 const remainingAmount = isRemainingPayment
41 ? parseFloat(window.yatraBookingData?.remainingAmount ?? $form.data('payment-due')) || 0
42 : 0;
43 const totalRemainingAmount = isRemainingPayment
44 ? parseFloat(window.yatraBookingData?.totalAmount ?? (remainingAmount + (window.yatraBookingData?.amountPaid || 0))) || remainingAmount
45 : null;
46
47 // Get pricing data
48 const pricePerPerson = parseFloat($('input[name="trip_price"]').val()) || window.yatraBookingData?.tripPrice || 0;
49 // Use global currency from settings (prioritize yatraBookingData which uses global currency)
50 const currency = window.yatraBookingData?.currency || $('input[name="currency"]').val() || 'USD';
51 const depositPercentage = window.yatraBookingData?.depositPercentage || 20;
52 const partialPercentage = window.yatraBookingData?.partialPercentage || 30;
53
54 window.yatraBookingSummary = window.yatraBookingSummary || {};
55
56 // Coupon state
57 let appliedCoupon = null;
58
59 // Group discount state (used for dynamic updates when traveler count changes)
60 let appliedGroupDiscount = null;
61 const groupDiscounts = window.yatraBookingData?.groupDiscounts || [];
62
63 // Country options for dynamically added travelers
64 const countryOptions = generateCountryOptions();
65
66 /**
67 * Generate country select options HTML
68 */
69 function generateCountryOptions() {
70 const countries = {
71 'AF': 'Afghanistan', 'AL': 'Albania', 'DZ': 'Algeria', 'AR': 'Argentina',
72 'AU': 'Australia', 'AT': 'Austria', 'BD': 'Bangladesh', 'BE': 'Belgium',
73 'BR': 'Brazil', 'BT': 'Bhutan', 'CA': 'Canada', 'CN': 'China',
74 'CO': 'Colombia', 'CZ': 'Czech Republic', 'DK': 'Denmark', 'EG': 'Egypt',
75 'FI': 'Finland', 'FR': 'France', 'DE': 'Germany', 'GR': 'Greece',
76 'HK': 'Hong Kong', 'HU': 'Hungary', 'IS': 'Iceland', 'IN': 'India',
77 'ID': 'Indonesia', 'IE': 'Ireland', 'IL': 'Israel', 'IT': 'Italy',
78 'JP': 'Japan', 'KE': 'Kenya', 'KR': 'South Korea', 'MY': 'Malaysia',
79 'MV': 'Maldives', 'MX': 'Mexico', 'NL': 'Netherlands', 'NZ': 'New Zealand',
80 'NP': 'Nepal', 'NO': 'Norway', 'PK': 'Pakistan', 'PE': 'Peru',
81 'PH': 'Philippines', 'PL': 'Poland', 'PT': 'Portugal', 'RO': 'Romania',
82 'RU': 'Russia', 'SA': 'Saudi Arabia', 'SG': 'Singapore', 'ZA': 'South Africa',
83 'ES': 'Spain', 'LK': 'Sri Lanka', 'SE': 'Sweden', 'CH': 'Switzerland',
84 'TW': 'Taiwan', 'TH': 'Thailand', 'TR': 'Turkey', 'AE': 'United Arab Emirates',
85 'GB': 'United Kingdom', 'US': 'United States', 'VN': 'Vietnam'
86 };
87
88 let options = '<option value="">Select Country</option>';
89 for (const [code, name] of Object.entries(countries)) {
90 options += `<option value="${code}">${name}</option>`;
91 }
92 return options;
93 }
94
95 /**
96 * Get current traveler count
97 */
98 function getTravelerCount() {
99 // Check if we have category-based pricing (dropdown style)
100 const categoryInputs = $('.yatra-qty-input[data-category-id]');
101 if (categoryInputs.length > 0) {
102 let total = 0;
103 categoryInputs.each(function() {
104 total += parseInt($(this).val()) || 0;
105 });
106 return total || 1;
107 }
108
109 // Fallback to simple number input or traveler forms
110 const simpleInput = $('#number-of-travelers').val();
111 if (simpleInput) {
112 return parseInt(simpleInput) || 1;
113 }
114
115 return $('.yatra-traveler-form').length || 1;
116 }
117
118 /**
119 * Check if using traveler-based pricing
120 */
121 function isTravelerBasedPricing() {
122 return $('.yatra-summary-pricing').data('pricing-type') === 'traveler_based';
123 }
124
125 /**
126 * Calculate total for traveler-based pricing
127 */
128 function calculateTravelerBasedTotal() {
129 let total = 0;
130 $('.yatra-qty-input[data-category-id]').each(function() {
131 const count = parseInt($(this).val()) || 0;
132 const price = parseFloat($(this).data('price')) || 0;
133 const pricingMode = $(this).data('pricing-mode') || $(this).closest('.yatra-quantity-row').data('pricing-mode') || 'per_person';
134
135 if (pricingMode === 'per_group') {
136 // Per group: charge flat price once if any travelers in this category
137 if (count > 0) {
138 total += price;
139 }
140 } else {
141 // Per person: charge per traveler
142 total += count * price;
143 }
144 });
145 return total;
146 }
147
148 /**
149 * Debounce timer for AJAX calls
150 */
151 let summaryDebounceTimer = null;
152
153 /**
154 * Update the dropdown display text for traveler-based pricing
155 */
156 function updateTravelersDisplayText() {
157 const parts = [];
158 $('.yatra-qty-input[data-category-id]').each(function() {
159 const count = parseInt($(this).val()) || 0;
160 const categoryLabel = $(this).closest('.yatra-quantity-row').find('.yatra-quantity-title').text();
161 if (count > 0) {
162 parts.push(categoryLabel + ' x ' + count);
163 }
164 });
165 const displayText = parts.length > 0 ? parts.join(', ') : 'Select travelers';
166 $('#yatra-travelers-display').text(displayText);
167 }
168
169 /**
170 * Update display text for regular pricing
171 */
172 function updateRegularTravelersDisplayText(count) {
173 const label = count === 1 ? 'traveler' : 'travelers';
174 $('#yatra-travelers-display-regular').text(count + ' ' + label);
175 }
176
177
178 /**
179 * Format currency
180 */
181 function formatCurrency(amount, currencyCode) {
182 // Get currency formatting settings from global settings
183 const currencyPosition = window.yatraBookingData?.currencyPosition || 'before';
184 let decimalPlaces = parseInt(String(window.yatraBookingData?.decimalPlaces ?? '2'), 10);
185 if (Number.isNaN(decimalPlaces)) {
186 decimalPlaces = 2;
187 }
188 decimalPlaces = Math.max(0, Math.min(4, decimalPlaces));
189 const thousandSeparator = window.yatraBookingData?.thousandSeparator || ',';
190 const decimalSeparator = window.yatraBookingData?.decimalSeparator || '.';
191
192 // Currency symbols matching PHP yatra_get_currency_symbol function
193 const symbols = {
194 'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'CNY': '¥',
195 'INR': '₹', 'NPR': 'Rs', 'AUD': 'A$', 'CAD': 'C$', 'CHF': 'CHF',
196 'NZD': 'NZ$', 'SGD': 'S$', 'HKD': 'HK$', 'KRW': '₩', 'THB': '฿',
197 'MYR': 'RM', 'PHP': '₱', 'IDR': 'Rp', 'VND': '₫', 'BRL': 'R$',
198 'MXN': 'MX$', 'RUB': '₽', 'ZAR': 'R', 'AED': 'د.إ', 'SAR': '﷼',
199 'TRY': '₺', 'SEK': 'kr', 'NOK': 'kr', 'DKK': 'kr', 'PLN': 'zł',
200 'CZK': 'Kč', 'HUF': 'Ft', 'ILS': '₪', 'TWD': 'NT$', 'PKR': '₨',
201 'BDT': '৳', 'LKR': 'Rs', 'EGP': 'E£', 'NGN': '₦', 'KES': 'KSh',
202 'GHS': 'GH₵', 'ARS': 'AR$', 'CLP': 'CL$', 'COP': 'CO$', 'PEN': 'S/'
203 };
204
205 const symbol = symbols[currencyCode?.toUpperCase()] || (currencyCode ? currencyCode + ' ' : '');
206
207 // Format amount with proper separators
208 // First format with standard separators, then replace with custom ones
209 const tempFormatted = amount.toLocaleString('en-US', {
210 minimumFractionDigits: decimalPlaces,
211 maximumFractionDigits: decimalPlaces
212 });
213 // Replace thousand separator
214 const formattedAmount = tempFormatted
215 .split(',').join('__THOUSAND__')
216 .split('.').join('__DECIMAL__')
217 .replace(/__THOUSAND__/g, thousandSeparator)
218 .replace(/__DECIMAL__/g, decimalSeparator);
219
220 // Position currency based on settings (before or after)
221 if (currencyPosition === 'right' || currencyPosition === 'after') {
222 return formattedAmount + ' ' + symbol;
223 }
224
225 return symbol + ' ' + formattedAmount;
226 }
227
228 /**
229 * Build JSON body for /booking/summary (payment method, travelers, services).
230 * Keeps sidebar + window.yatraBookingSummary aligned with Pro flexible payments.
231 */
232 function buildPricingSummaryPayload() {
233 const payload = {};
234 const pm = $('input[name="payment_method"]:checked').val();
235 if (pm) {
236 payload.payment_method = pm;
237 }
238 const travelDate = $('#travel-date').val() || $('input[name="travel_date"]').first().val();
239 if (travelDate) {
240 payload.travel_date = travelDate;
241 }
242 const depTime = $('input[name="departure_time"]').first().val();
243 if (depTime) {
244 payload.departure_time = depTime;
245 }
246 if ($('.yatra-qty-input[data-category-id]').length) {
247 const travelerCounts = {};
248 $('.yatra-qty-input[data-category-id]').each(function() {
249 const catId = $(this).data('category-id');
250 travelerCounts[catId] = parseInt($(this).val(), 10) || 0;
251 });
252 payload.traveler_counts = travelerCounts;
253 }
254 const serviceIds = [];
255 $('#yatra-additional-services input[type="checkbox"]:checked').each(function() {
256 const id = parseInt($(this).closest('.yatra-service-item').data('service-id'), 10);
257 if (!Number.isNaN(id)) {
258 serviceIds.push(id);
259 }
260 });
261 if (serviceIds.length) {
262 payload.additional_services = serviceIds;
263 }
264 const pricingType = $('#yatra-summary-pricing').data('pricing-type');
265 if (pricingType) {
266 payload.pricing_type = pricingType;
267 }
268 return payload;
269 }
270
271 /**
272 * Update pay button label and amount (offline gateways still show amount due for deposit/partial).
273 */
274 function updateCheckoutButtonState() {
275 const $buttonText = $('#pay-button-text');
276 const $payAmount = $('#pay-amount');
277 const gateway = $('input[name="payment_gateway"]:checked').val() || 'pay_later';
278 const offlineGateways = ['pay_later', 'bank_transfer'];
279 const isOffline = offlineGateways.includes(gateway);
280 const paymentMethod = $('input[name="payment_method"]:checked').val() || 'full';
281 const summary = window.yatraBookingSummary || {};
282 let due = parseFloat(summary.amountDue);
283 const total = parseFloat(summary.totalAmount);
284 if (Number.isNaN(due)) {
285 due = parseFloat($form.attr('data-payment-due')) || 0;
286 }
287 let totalSafe = total;
288 if (Number.isNaN(totalSafe)) {
289 totalSafe = parseFloat($form.attr('data-payment-due')) || 0;
290 }
291 const flexDue = paymentMethod === 'deposit' || paymentMethod === 'partial';
292
293 if (isOffline) {
294 $buttonText.text('Complete Booking');
295 const showAmount = flexDue ? due : totalSafe;
296 if (showAmount > 0) {
297 $payAmount.text(formatCurrency(showAmount, currency)).show();
298 } else {
299 $payAmount.hide();
300 }
301 } else {
302 $buttonText.text('Pay Now');
303 const payVal = flexDue && due > 0 ? due : totalSafe;
304 if (payVal > 0) {
305 $payAmount.text(formatCurrency(payVal, currency)).show();
306 } else {
307 $payAmount.hide();
308 }
309 }
310 }
311
312 /**
313 * Add a new traveler form dynamically
314 */
315 function addTravelerForm(index) {
316 const $container = $('#yatra-travelers-container');
317
318 // Get field configuration from the first traveler form
319 const $firstTraveler = $container.find('.yatra-traveler-form').first();
320 if (!$firstTraveler.length) return;
321
322 // Clone the first traveler form
323 const $newTraveler = $firstTraveler.clone();
324
325 // Update index references
326 $newTraveler.attr('data-traveler-index', index);
327 $newTraveler.find('.yatra-traveler-title').text('Traveler ' + index);
328
329 // Add "Additional traveler" note
330 if (!$newTraveler.find('.yatra-traveler-note').length) {
331 $newTraveler.find('.yatra-traveler-header').append('<span class="yatra-traveler-note">Additional traveler</span>');
332 }
333
334 // Update all field IDs and names
335 $newTraveler.find('input, select, textarea').each(function() {
336 const $field = $(this);
337 const oldId = $field.attr('id');
338 const oldName = $field.attr('name');
339
340 if (oldId) {
341 // Replace traveler-1- with traveler-{index}-
342 const newId = oldId.replace(/traveler-\d+-/, 'traveler-' + index + '-');
343 $field.attr('id', newId);
344 }
345
346 if (oldName) {
347 // Replace travelers[1] with travelers[{index}]
348 const newName = oldName.replace(/travelers\[\d+\]/, 'travelers[' + index + ']');
349 $field.attr('name', newName);
350 }
351
352 // Clear values
353 $field.val('');
354 $field.removeClass('error');
355 });
356
357 // Update associated labels
358 $newTraveler.find('label').each(function() {
359 const $label = $(this);
360 const forAttr = $label.attr('for');
361 if (forAttr) {
362 const newFor = forAttr.replace(/traveler-\d+-/, 'traveler-' + index + '-');
363 $label.attr('for', newFor);
364 }
365 });
366
367 $container.append($newTraveler);
368 }
369
370 /**
371 * Remove the last traveler form
372 */
373 function removeTravelerForm(index) {
374 const $container = $('#yatra-travelers-container');
375 $container.find(`.yatra-traveler-form[data-traveler-index="${index}"]`).remove();
376 }
377
378 /**
379 * Convert FormData to nested object structure
380 */
381 function formDataToObject(formData) {
382 const obj = {};
383
384 for (const [key, value] of formData.entries()) {
385 // Handle array notation like travelers[1][first_name]
386 const matches = key.match(/^([^\[]+)(?:\[([^\]]*)\])?(?:\[([^\]]*)\])?$/);
387
388 if (matches) {
389 const [, base, index, field] = matches;
390
391 if (index !== undefined && field !== undefined) {
392 // Array of objects: travelers[1][first_name]
393 if (!obj[base]) obj[base] = {};
394 if (!obj[base][index]) obj[base][index] = {};
395 obj[base][index][field] = value;
396 } else if (index !== undefined) {
397 // Simple array: items[0]
398 if (!obj[base]) obj[base] = [];
399 obj[base][index] = value;
400 } else {
401 // Simple key
402 obj[key] = value;
403 }
404 } else {
405 obj[key] = value;
406 }
407 }
408
409 // Convert travelers object to array
410 if (obj.travelers && typeof obj.travelers === 'object') {
411 obj.travelers = Object.values(obj.travelers);
412 }
413
414 return obj;
415 }
416
417 /**
418 * Validate form before submission
419 */
420 function validateForm() {
421 if (isRemainingPayment) {
422 return true;
423 }
424
425 let isValid = true;
426 const errors = [];
427
428 // Remove previous error states
429 $form.find('.error').removeClass('error');
430 $('.yatra-form-error').remove();
431
432 // Validate all required fields
433 $form.find('input[required], select[required], textarea[required]').each(function() {
434 const $field = $(this);
435 const value = $field.val();
436
437 if (!value || value.trim() === '') {
438 isValid = false;
439 $field.addClass('error');
440 const label = $field.closest('.yatra-form-group').find('label').text().replace('*', '').trim();
441 errors.push(label + ' is required');
442 }
443 });
444
445 // Validate email format
446 const emailFields = $form.find('input[type="email"]');
447 emailFields.each(function() {
448 const $field = $(this);
449 const value = $field.val();
450 if (value && !isValidEmail(value)) {
451 isValid = false;
452 $field.addClass('error');
453 errors.push('Please enter a valid email address');
454 }
455 });
456
457 // Validate terms checkbox
458 if (!$('input[name="accept_terms"]').is(':checked')) {
459 isValid = false;
460 errors.push('Please accept the Terms and Conditions');
461 }
462
463 // Validate privacy checkbox
464 if (!$('input[name="accept_privacy"]').is(':checked')) {
465 isValid = false;
466 errors.push('Please accept the Privacy Policy');
467 }
468
469 if (!isValid && errors.length > 0) {
470 showFormError(errors[0]);
471 }
472
473 return isValid;
474 }
475
476 /**
477 * Validate email format
478 */
479 function isValidEmail(email) {
480 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
481 }
482
483 /**
484 * Show form error message
485 */
486 function showFormError(message) {
487 const $errorContainer = $('#yatra-booking-form-errors');
488 if ($errorContainer.length) {
489 $errorContainer.html('<div class="yatra-form-error" role="alert">' + message + '</div>');
490 $('html, body').animate({ scrollTop: $errorContainer.offset().top - 100 }, 300);
491 } else {
492 alert(message);
493 }
494 }
495
496 /**
497 * Show success message
498 * @param {string} message
499 * @param {string} reference
500 * @param {{ heading?: string, footerHtml?: string, iconColor?: string }|undefined} opts
501 */
502 function showSuccessMessage(message, reference, opts) {
503 opts = opts || {};
504 const heading = opts.heading || 'Booking Confirmed!';
505 const iconColor = opts.iconColor || '#22c55e';
506 const footerHtml = opts.footerHtml !== undefined
507 ? opts.footerHtml
508 : '<p style="margin-top: 24px; color: #6b7280;">A confirmation email has been sent to your email address.</p>';
509 const $success = $('<div class="yatra-booking-success" style="text-align: center; padding: 60px 20px;">' +
510 '<svg style="width: 80px; height: 80px; color: ' + iconColor + '; margin-bottom: 24px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
511 '<circle cx="12" cy="12" r="10"></circle><polyline points="9,12 12,15 16,10"></polyline></svg>' +
512 '<h2 style="font-size: 28px; margin-bottom: 12px; color: #111827;">' + heading + '</h2>' +
513 '<p style="font-size: 18px; color: #6b7280; margin-bottom: 8px;">' + message + '</p>' +
514 '<p style="font-size: 16px; color: #111827; font-weight: 600;">Reference: ' + reference + '</p>' +
515 footerHtml +
516 '</div>');
517
518 $form.html($success);
519 }
520
521 // =====================
522 // Event Handlers
523 // =====================
524
525 // Dropdown toggle for traveler selector
526 $(document).on('click', '.yatra-booking-participants-select', function(e) {
527 // Don't toggle if clicking on buttons or inputs inside
528 if ($(e.target).closest('.yatra-qty-btn, .yatra-qty-input, .yatra-quantity-controls').length) {
529 return;
530 }
531
532 const $select = $(this);
533 const wasActive = $select.hasClass('active');
534
535 // Close all dropdowns first
536 $('.yatra-booking-participants-select').removeClass('active');
537
538 // Toggle this one
539 if (!wasActive) {
540 $select.addClass('active');
541 }
542 });
543
544 // Close dropdown when clicking outside
545 $(document).on('click', function(e) {
546 if (!$(e.target).closest('.yatra-booking-participants-select').length) {
547 $('.yatra-booking-participants-select').removeClass('active');
548 }
549 });
550
551 // Quantity selector for regular pricing (dropdown style)
552 $(document).on('click', '.yatra-qty-btn[data-field="travelers"]', function(e) {
553 e.preventDefault();
554 e.stopPropagation();
555
556 const $btn = $(this);
557 const $input = $('#number-of-travelers');
558 let currentValue = parseInt($input.val()) || 1;
559 const min = parseInt($input.attr('min')) || 1;
560 const max = parseInt($input.attr('max')) || 20;
561
562 if ($btn.hasClass('yatra-qty-plus')) {
563 if (currentValue < max) {
564 currentValue++;
565 addTravelerForm(currentValue);
566 }
567 } else if ($btn.hasClass('yatra-qty-minus')) {
568 if (currentValue > min) {
569 removeTravelerForm(currentValue);
570 currentValue--;
571 }
572 }
573
574 $input.val(currentValue);
575
576 // Update button disabled states
577 $btn.closest('.yatra-quantity-controls').find('.yatra-qty-minus').prop('disabled', currentValue <= min);
578 $btn.closest('.yatra-quantity-controls').find('.yatra-qty-plus').prop('disabled', currentValue >= max);
579
580 updateBookingSession();
581 });
582
583 // Quantity selector for traveler-based category pricing (dropdown style)
584 $(document).on('click', '.yatra-qty-btn[data-category]', function(e) {
585 e.preventDefault();
586 e.stopPropagation();
587
588 const $btn = $(this);
589 const $row = $btn.closest('.yatra-quantity-row');
590 const $input = $row.find('.yatra-qty-input');
591 let currentValue = parseInt($input.val()) || 0;
592 const min = parseInt($input.attr('min')) || 0;
593 const max = parseInt($input.attr('max')) || 20;
594
595 if ($btn.hasClass('yatra-qty-plus')) {
596 if (currentValue < max) {
597 currentValue++;
598 }
599 } else if ($btn.hasClass('yatra-qty-minus')) {
600 if (currentValue > min) {
601 currentValue--;
602 }
603 }
604
605 $input.val(currentValue);
606
607 // Update button disabled states
608 $row.find('.yatra-qty-minus').prop('disabled', currentValue <= min);
609 $row.find('.yatra-qty-plus').prop('disabled', currentValue >= max);
610
611 // Update traveler forms based on total count
612 updateTravelerFormsForCategories();
613
614 // Update travelers display text immediately
615 updateTravelersDisplayText();
616
617
618 updateBookingSession();
619 });
620
621 /**
622 * Update traveler forms based on category selections
623 */
624 function updateTravelerFormsForCategories() {
625 const targetCount = getTravelerCount();
626 const currentFormCount = $('.yatra-traveler-form').length;
627
628 if (targetCount > currentFormCount) {
629 // Add forms
630 for (let i = currentFormCount + 1; i <= targetCount; i++) {
631 addTravelerForm(i);
632 }
633 } else if (targetCount < currentFormCount && targetCount > 0) {
634 // Remove forms
635 for (let i = currentFormCount; i > targetCount; i--) {
636 removeTravelerForm(i);
637 }
638 }
639 }
640
641 /**
642 * Update booking session with new traveler counts
643 * This ensures checkout/confirmation reflects the updated travelers
644 */
645 function updateBookingSession() {
646 const sessionPayload = {
647 travelers: getTravelerCount()
648 };
649
650 // Add traveler_counts for category-based pricing
651 if (isTravelerBasedPricing()) {
652 const travelerCounts = {};
653 $('.yatra-qty-input[data-category-id]').each(function() {
654 const categoryId = $(this).data('category-id');
655 const count = parseInt($(this).val()) || 0;
656 if (categoryId !== undefined) {
657 travelerCounts[categoryId] = count;
658 }
659 });
660 sessionPayload.traveler_counts = travelerCounts;
661 }
662
663 // Update session via REST API (needs session cookie, but no nonce)
664 const { base: restBase, isPlain } = getRestBase();
665 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
666 let sessionUrl;
667 if (isPlain) {
668 sessionUrl = `${siteBase}/?rest_route=/yatra/v1/booking/session`;
669 } else if (restBase.includes('/yatra/v1')) {
670 sessionUrl = `${restBase}/booking/session`;
671 } else {
672 sessionUrl = `${restBase}/yatra/v1/booking/session`;
673 }
674
675 fetch(sessionUrl, {
676 method: 'POST',
677 credentials: 'same-origin',
678 headers: {
679 'Content-Type': 'application/json',
680 'X-WP-Nonce': nonce
681 },
682 body: JSON.stringify(sessionPayload)
683 })
684 .then(response => response.json())
685 .then(data => {
686 if (!data.success) {
687 console.warn('Failed to update booking session:', data.message);
688 return;
689 }
690 schedulePricingSummaryRefresh();
691 })
692 .catch(error => {
693 console.error('Error updating booking session:', error);
694 });
695 }
696
697 // Initial summary figures from server-rendered sidebar (before any AJAX)
698 if (!isRemainingPayment) {
699 const $sbInit = $('.yatra-booking-summary').first();
700 if ($sbInit.length) {
701 const t = parseFloat($sbInit.attr('data-summary-total'));
702 const du = parseFloat($sbInit.attr('data-summary-due'));
703 window.yatraBookingSummary = {
704 totalAmount: Number.isNaN(t) ? null : t,
705 amountDue: Number.isNaN(du) ? null : du,
706 amountPaid: 0
707 };
708 }
709 updateCheckoutButtonState();
710 }
711
712 $(document).on('change', 'input[name="payment_method"]', function() {
713 schedulePricingSummaryRefresh();
714 });
715
716 // Update when gateway changes
717 $('input[name="payment_gateway"]').on('change', function() {
718
719 // Show gateway-specific info
720 const gateway = $(this).val();
721 const $gatewayInfo = $('#yatra-gateway-info');
722 const $gatewayDetails = $('#yatra-gateway-details');
723
724 const gatewayMessages = {
725 'pay_later': '<p>Your booking will be reserved. Full payment is required before the trip date.</p>',
726 'bank_transfer': '<p>After completing your booking, you will receive bank details via email. Your booking will be confirmed once payment is received.</p>',
727 'stripe': '<p>You will be securely redirected to complete your payment with credit or debit card.</p>',
728 'paypal': '<p>You will be redirected to PayPal to complete your payment securely.</p>',
729 'razorpay': '<p>You will be redirected to Razorpay to complete your payment.</p>',
730 'esewa': '<p>You will be redirected to eSewa to complete your payment.</p>',
731 'khalti': '<p>You will be redirected to Khalti to complete your payment.</p>',
732 };
733
734 if (gatewayMessages[gateway] && $gatewayDetails.length) {
735 $gatewayDetails.html(gatewayMessages[gateway]);
736 $gatewayInfo.show();
737 } else if ($gatewayInfo.length) {
738 $gatewayInfo.hide();
739 }
740
741 updateCheckoutButtonState();
742
743 // Toggle gateway-specific content containers
744 $('.yatra-gateway-extra').removeClass('active');
745 const $gatewayExtra = $('#yatra-gateway-extra-' + gateway);
746 // Only show if it has content (children elements)
747 if ($gatewayExtra.length && $gatewayExtra.children().length > 0) {
748 $gatewayExtra.addClass('active');
749 }
750 });
751
752 // Ensure default-selected gateway shows its content on load
753 const $defaultGateway = $('input[name="payment_gateway"]:checked');
754 if ($defaultGateway.length) {
755 $defaultGateway.trigger('change');
756 }
757
758 /**
759 * ============================================
760 * CENTRALIZED PAYMENT GATEWAY HOOK SYSTEM
761 * ============================================
762 *
763 * Events:
764 * - yatra_booking_submit: Fired when form is submitted, gateways can intercept
765 * - yatra_payment_response: Fired when server responds with payment data
766 * - yatra_payment_success: Fired when payment completes successfully
767 * - yatra_payment_failed: Fired when payment fails
768 * - yatra_payment_cancelled: Fired when user cancels payment
769 */
770
771 // Store for registered gateway handlers
772 window.yatraPaymentGateways = window.yatraPaymentGateways || {};
773
774 /**
775 * Register a payment gateway handler
776 * @param {string} gatewayId - Gateway identifier (e.g., 'stripe', 'razorpay')
777 * @param {object} handler - Handler object with methods: canHandle, handlePayment
778 */
779 window.yatraRegisterPaymentGateway = function(gatewayId, handler) {
780
781 window.yatraPaymentGateways[gatewayId] = handler;
782 };
783
784 /**
785 * Handle payment response from server
786 */
787 function handlePaymentResponse(response, originalBtnHtml) {
788
789
790
791 if (!response.success) {
792 const errMsg = response.message || 'An error occurred. Please try again.';
793 showFormError(errMsg);
794 $submitBtn.prop('disabled', false).html(originalBtnHtml);
795 return;
796 }
797
798 const data = response.data || {};
799
800 if (data.waitlist) {
801 const reference = data.reference || 'N/A';
802 showSuccessMessage(
803 response.message || "You're on the waitlist for this departure.",
804 reference,
805 {
806 heading: "You're on the waitlist",
807 iconColor: '#ca8a04',
808 footerHtml: '<p style="margin-top: 24px; color: #6b7280;">We will contact you if a space opens up.</p>'
809 }
810 );
811 return;
812 }
813
814
815 // Check for redirect URLs first (PayPal, eSewa, Khalti, etc.)
816 if (data.payment_url) {
817
818 window.location.href = data.payment_url;
819 return;
820 }
821
822 if (data.redirect_url && !data.requires_action) {
823
824 window.location.href = data.redirect_url;
825 return;
826 }
827
828 // Check for client-side payment actions (Stripe, Razorpay, Square, etc.)
829 if (data.requires_action) {
830
831
832 // Dispatch unified payment event
833 const paymentEvent = new CustomEvent('yatra_payment_response', {
834 detail: {
835 ...data,
836 originalBtnHtml,
837 resetButton: () => $submitBtn.prop('disabled', false).html(originalBtnHtml)
838 }
839 });
840 document.dispatchEvent(paymentEvent);
841
842 // Setup failure/cancel listeners
843 const cleanup = () => {
844 document.removeEventListener('yatra_payment_failed', onFailed);
845 document.removeEventListener('yatra_payment_cancelled', onCancelled);
846 };
847 const onFailed = (e) => {
848 showFormError(e.detail?.error || 'Payment failed. Please try again.');
849 $submitBtn.prop('disabled', false).html(originalBtnHtml);
850 cleanup();
851 };
852 const onCancelled = () => {
853 $submitBtn.prop('disabled', false).html(originalBtnHtml);
854 cleanup();
855 };
856 document.addEventListener('yatra_payment_failed', onFailed, { once: true });
857 document.addEventListener('yatra_payment_cancelled', onCancelled, { once: true });
858 return;
859 }
860
861 // Default: show success message
862 const reference = data.reference || 'N/A';
863 showSuccessMessage(response.message || 'Success!', reference);
864 }
865
866 // Form submission - using FormData API
867 $form.on('submit', function(e) {
868 e.preventDefault();
869
870 // Validate form
871 if (!validateForm()) {
872 return;
873 }
874
875 const originalBtnHtml = $submitBtn.html();
876 const selectedGateway = $('input[name="payment_gateway"]:checked').val();
877
878 // Collect form data using FormData API
879 const formData = new FormData(this);
880
881 // Convert to structured object
882 const bookingData = formDataToObject(formData);
883 const summary = window.yatraBookingSummary || {};
884
885 // Add checkbox values explicitly (unchecked checkboxes aren't in FormData)
886 bookingData.accept_terms = $('input[name="accept_terms"]').is(':checked');
887 bookingData.accept_privacy = $('input[name="accept_privacy"]').is(':checked');
888 bookingData.subscribe_newsletter = $('input[name="subscribe_newsletter"]').is(':checked');
889 bookingData.payment_due = parseFloat($form.attr('data-payment-due')) || window.yatraBookingData?.paymentDue || null;
890 bookingData.total_amount = summary.totalAmount ?? null;
891 bookingData.amount_due = summary.amountDue ?? null;
892 bookingData.amount_paid = summary.amountPaid ?? null;
893 bookingData.currency = window.yatraBookingData?.currency || bookingData.currency || $('input[name="currency"]').val() || 'USD';
894
895 // Dispatch unified booking submit event - gateways can intercept
896 const submitEvent = new CustomEvent('yatra_booking_submit', {
897 detail: {
898 gateway: selectedGateway,
899 form: this,
900 bookingData,
901 submitButton: $submitBtn[0],
902 originalBtnHtml,
903 // Helper to proceed with default submission
904 proceedWithSubmission: () => submitBookingToServer(bookingData, originalBtnHtml)
905 },
906 cancelable: true
907 });
908
909
910 const shouldProceed = document.dispatchEvent(submitEvent);
911
912 // If event was not cancelled, proceed with server submission
913 if (shouldProceed) {
914 submitBookingToServer(bookingData, originalBtnHtml);
915 }
916 });
917
918 /**
919 * Submit booking to server
920 */
921 function submitBookingToServer(bookingData, originalBtnHtml) {
922 const { base: restBase, isPlain } = getRestBase();
923 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
924 let createUrl;
925 if (isPlain) {
926 createUrl = `${siteBase}/?rest_route=/yatra/v1/booking/create`;
927 } else if (restBase.includes('/yatra/v1')) {
928 createUrl = `${restBase}/booking/create`;
929 } else {
930 createUrl = `${restBase}/yatra/v1/booking/create`;
931 }
932
933 // Show loading state
934 $submitBtn.prop('disabled', true).html(
935 '<svg class="animate-spin" style="display: inline-block; width: 20px; height: 20px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
936 '<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle></svg>' +
937 '<span>Processing...</span>'
938 );
939
940 fetch(createUrl, {
941 method: 'POST',
942 headers: {
943 'Content-Type': 'application/json',
944 'X-WP-Nonce': nonce
945 },
946 credentials: 'same-origin',
947 body: JSON.stringify(bookingData)
948 })
949 .then(response => response.json())
950 .then(response => handlePaymentResponse(response, originalBtnHtml))
951 .catch(error => {
952 console.error('Error:', error);
953 showFormError('An error occurred. Please try again.');
954 $submitBtn.prop('disabled', false).html(originalBtnHtml);
955 });
956 }
957
958 // Toggle coupon form visibility
959 $('#yatra-coupon-toggle-btn').on('click', function() {
960 const $form = $('#yatra-coupon-form');
961 const $chevron = $(this).find('.yatra-coupon-chevron');
962
963 if ($form.is(':visible')) {
964 $form.slideUp(200);
965 $chevron.css('transform', 'rotate(0deg)');
966 } else {
967 $form.slideDown(200);
968 $chevron.css('transform', 'rotate(180deg)');
969 // Focus the input field
970 setTimeout(() => $('#yatra-coupon-code').focus(), 200);
971 }
972 });
973
974 // Apply coupon
975 $('#yatra-apply-coupon').on('click', function() {
976 applyCoupon();
977 });
978
979 // Apply coupon on Enter key
980 $('#yatra-coupon-code').on('keypress', function(e) {
981 if (e.which === 13) {
982 e.preventDefault();
983 applyCoupon();
984 }
985 });
986
987 // Remove coupon
988 $('#yatra-remove-coupon').on('click', function() {
989 removeCoupon();
990 });
991
992 function applyCoupon() {
993 const code = $('#yatra-coupon-code').val().trim().toUpperCase();
994 const $btn = $('#yatra-apply-coupon');
995 const $message = $('#yatra-coupon-message');
996
997 if (!code) {
998 showCouponMessage('Please enter a coupon code.', 'error');
999 return;
1000 }
1001
1002 // Show loading state
1003 $btn.prop('disabled', true).text('Applying...');
1004 $message.hide();
1005
1006 fetch(apiUrl + '/booking/coupon/apply', {
1007 method: 'POST',
1008 headers: {
1009 'Content-Type': 'application/json',
1010 'X-WP-Nonce': nonce
1011 },
1012 credentials: 'same-origin',
1013 body: JSON.stringify({ code: code })
1014 })
1015 .then(response => response.json())
1016 .then(response => {
1017 $btn.prop('disabled', false).text('Apply');
1018
1019 if (response.success) {
1020 appliedCoupon = response.data;
1021
1022 // Hide the form and show applied coupon
1023 $('#yatra-coupon-form').hide();
1024 $('#yatra-coupon-toggle-btn').hide();
1025
1026 // Show applied coupon display
1027 const $applied = $('#yatra-applied-coupon');
1028 $applied.find('.yatra-coupon-code-display').text(response.data.code);
1029 $applied.find('.yatra-coupon-discount').text('-' + response.data.discount_formatted);
1030 $applied.show();
1031
1032 // Update price display
1033 updatePriceWithDiscount(response.data);
1034
1035 showCouponMessage(response.message, 'success');
1036 } else {
1037 showCouponMessage(response.message || 'Invalid coupon code.', 'error');
1038 }
1039 })
1040 .catch(error => {
1041 console.error('Coupon error:', error);
1042 $btn.prop('disabled', false).text('Apply');
1043 showCouponMessage('An error occurred. Please try again.', 'error');
1044 });
1045 }
1046
1047 function removeCoupon() {
1048 const $btn = $('#yatra-remove-coupon');
1049
1050 $btn.prop('disabled', true);
1051
1052 fetch(apiUrl + '/booking/coupon/remove', {
1053 method: 'POST',
1054 headers: {
1055 'Content-Type': 'application/json',
1056 'X-WP-Nonce': nonce
1057 },
1058 credentials: 'same-origin',
1059 body: JSON.stringify({})
1060 })
1061 .then(response => response.json())
1062 .then(response => {
1063 $btn.prop('disabled', false);
1064
1065 if (response.success) {
1066 appliedCoupon = null;
1067
1068 // Hide applied coupon and show toggle button
1069 $('#yatra-applied-coupon').hide();
1070 $('#yatra-coupon-toggle-btn').show();
1071 $('#yatra-coupon-code').val('');
1072 $('#yatra-coupon-message').hide();
1073
1074 // Refresh pricing summary via AJAX
1075 refreshPricingSummary();
1076 }
1077 })
1078 .catch(error => {
1079 console.error('Remove coupon error:', error);
1080 $btn.prop('disabled', false);
1081 });
1082 }
1083
1084 function showCouponMessage(message, type) {
1085 const $message = $('#yatra-coupon-message');
1086 $message.removeClass('success error').addClass(type);
1087 $message.text(message).show();
1088
1089 // Auto-hide success messages
1090 if (type === 'success') {
1091 setTimeout(() => $message.fadeOut(), 3000);
1092 }
1093 }
1094
1095 function applySummaryResponse(response) {
1096 if (!response.success || !response.data) {
1097 return;
1098 }
1099 const d = response.data;
1100 if (d.pricing_html) {
1101 $('#yatra-summary-pricing').html(d.pricing_html);
1102 }
1103 window.yatraBookingSummary = {
1104 totalAmount: typeof d.total_amount === 'number' ? d.total_amount : parseFloat(d.total_amount),
1105 amountDue: typeof d.amount_due === 'number' ? d.amount_due : parseFloat(d.amount_due),
1106 amountPaid: typeof d.amount_paid === 'number' ? d.amount_paid : parseFloat(d.amount_paid || 0) || 0
1107 };
1108 const $sb = $('.yatra-booking-summary').first();
1109 if ($sb.length && !Number.isNaN(window.yatraBookingSummary.totalAmount)) {
1110 $sb.attr('data-summary-total', String(window.yatraBookingSummary.totalAmount));
1111 }
1112 if ($sb.length && !Number.isNaN(window.yatraBookingSummary.amountDue)) {
1113 $sb.attr('data-summary-due', String(window.yatraBookingSummary.amountDue));
1114 $form.attr('data-payment-due', String(window.yatraBookingSummary.amountDue));
1115 }
1116 updateCheckoutButtonState();
1117 }
1118
1119 function refreshPricingSummary(extraPayload) {
1120 const { base: restBase, isPlain } = getRestBase();
1121 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1122 let summaryUrl;
1123 if (isPlain) {
1124 summaryUrl = `${siteBase}/?rest_route=/yatra/v1/booking/summary`;
1125 } else if (restBase.includes('/yatra/v1')) {
1126 summaryUrl = `${restBase}/booking/summary`;
1127 } else {
1128 summaryUrl = `${restBase}/yatra/v1/booking/summary`;
1129 }
1130
1131 const body = Object.assign(buildPricingSummaryPayload(), extraPayload || {});
1132
1133 fetch(summaryUrl, {
1134 method: 'POST',
1135 headers: {
1136 'Content-Type': 'application/json',
1137 'X-WP-Nonce': nonce
1138 },
1139 credentials: 'same-origin',
1140 body: JSON.stringify(body)
1141 })
1142 .then(response => response.json())
1143 .then(response => {
1144 applySummaryResponse(response);
1145 })
1146 .catch(error => {
1147 console.error('Error refreshing pricing summary:', error);
1148 });
1149 }
1150
1151 function schedulePricingSummaryRefresh() {
1152 if (isRemainingPayment) {
1153 return;
1154 }
1155 clearTimeout(summaryDebounceTimer);
1156 summaryDebounceTimer = setTimeout(function() {
1157 refreshPricingSummary();
1158 }, 250);
1159 }
1160
1161 function updatePriceWithDiscount(couponData) {
1162 // Refresh pricing summary after coupon applied
1163 refreshPricingSummary();
1164 }
1165
1166 /**
1167 * Load existing coupon from session and display it
1168 */
1169 function loadCouponFromSession() {
1170 const { base: restBase, isPlain } = getRestBase();
1171 const siteBase = (window.yatraBookingData?.siteUrl || window.location.origin || '').replace(/\/$/, '');
1172 let sessionUrl;
1173 if (isPlain) {
1174 sessionUrl = `${siteBase}/?rest_route=/yatra/v1/booking/session`;
1175 } else if (restBase.includes('/yatra/v1')) {
1176 sessionUrl = `${restBase}/booking/session`;
1177 } else {
1178 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1179 }
1180
1181 fetch(sessionUrl, {
1182 method: 'GET',
1183 credentials: 'same-origin',
1184 headers: {
1185 'Content-Type': 'application/json',
1186 'X-WP-Nonce': nonce
1187 },
1188 })
1189 .then(response => response.json())
1190 .then(response => {
1191 if (response.success && response.data && response.data.coupon) {
1192 const coupon = response.data.coupon;
1193 // Calculate discount formatted if not present
1194 const discountFormatted = coupon.discount_formatted || formatCurrency(coupon.discount_amount || 0, currency);
1195
1196 appliedCoupon = {
1197 code: coupon.code,
1198 type: coupon.type,
1199 discount_amount: parseFloat(coupon.discount_amount) || 0,
1200 discount_formatted: discountFormatted,
1201 new_total: coupon.new_total || 0,
1202 new_total_formatted: coupon.new_total_formatted || ''
1203 };
1204
1205 // Hide the form and show applied coupon
1206 $('#yatra-coupon-form').hide();
1207 $('#yatra-coupon-toggle-btn').hide();
1208
1209 // Show applied coupon display
1210 const $applied = $('#yatra-applied-coupon');
1211 $applied.find('.yatra-coupon-code-display').text(coupon.code);
1212 $applied.find('.yatra-coupon-discount').text('-' + appliedCoupon.discount_formatted);
1213 $applied.show();
1214
1215 // Update price display
1216 updatePriceWithDiscount(appliedCoupon);
1217 }
1218 })
1219 .catch(error => {
1220 console.error('Error loading coupon from session:', error);
1221 });
1222 }
1223
1224 /**
1225 * Check URL for coupon parameter and auto-apply
1226 */
1227 function checkUrlForCoupon() {
1228 const urlParams = new URLSearchParams(window.location.search);
1229 const couponCode = urlParams.get('coupon');
1230
1231 if (couponCode && couponCode.trim()) {
1232 // Set the coupon code in the input
1233 $('#yatra-coupon-code').val(couponCode.trim().toUpperCase());
1234
1235 // Auto-apply the coupon
1236 applyCoupon();
1237
1238 // Remove coupon parameter from URL without page reload
1239 urlParams.delete('coupon');
1240 const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
1241 window.history.replaceState({}, '', newUrl);
1242 }
1243 }
1244
1245 // Check URL for coupon parameter first (takes priority)
1246 // If URL has coupon, it will be applied and session updated
1247 // If no URL coupon, load existing coupon from session
1248 const urlParams = new URLSearchParams(window.location.search);
1249 const urlCoupon = urlParams.get('coupon');
1250
1251 if (urlCoupon && urlCoupon.trim()) {
1252 // URL coupon takes priority - apply it
1253 checkUrlForCoupon();
1254 } else {
1255 // No URL coupon - load from session
1256 loadCouponFromSession();
1257 }
1258
1259 // Trigger gateway change to show initial info
1260 $('input[name="payment_gateway"]:checked').trigger('change');
1261
1262 // =====================
1263 // Additional Services Selection
1264 // =====================
1265
1266 // Handle additional service checkbox changes
1267 $(document).on('change', '#yatra-additional-services input[type="checkbox"]', function() {
1268 const $checkbox = $(this);
1269 const $serviceItem = $checkbox.closest('.yatra-service-item');
1270 const serviceId = $serviceItem.data('service-id');
1271 const servicePrice = parseFloat($serviceItem.data('service-price')) || 0;
1272 const servicePricePer = $serviceItem.data('service-price-per') || 'booking';
1273 const isRequired = $serviceItem.data('is-required') === 1 || $serviceItem.data('is-required') === '1';
1274 const isChecked = $checkbox.is(':checked');
1275
1276 // Prevent unchecking required services
1277 if (isRequired && !isChecked) {
1278 $checkbox.prop('checked', true);
1279 return;
1280 }
1281
1282 // Update visual state
1283 if (isChecked) {
1284 $serviceItem.addClass('yatra-service-selected');
1285 } else {
1286 $serviceItem.removeClass('yatra-service-selected');
1287 }
1288
1289 // Update booking session with selected services
1290 updateServicesInSession();
1291 });
1292
1293 /**
1294 * Get selected additional services
1295 */
1296 function getSelectedServices() {
1297 const services = [];
1298 $('#yatra-additional-services input[type="checkbox"]:checked').each(function() {
1299 const $serviceItem = $(this).closest('.yatra-service-item');
1300 services.push({
1301 id: parseInt($serviceItem.data('service-id')),
1302 price: parseFloat($serviceItem.data('service-price')) || 0,
1303 price_per: $serviceItem.data('service-price-per') || 'booking'
1304 });
1305 });
1306 return services;
1307 }
1308
1309 /**
1310 * Update services in booking session
1311 */
1312 function updateServicesInSession() {
1313 const selectedServices = getSelectedServices();
1314 const serviceIds = selectedServices.map(s => s.id);
1315
1316 const { base: restBase, isPlain } = getRestBase();
1317 let sessionUrl;
1318 if (isPlain && !restBase.includes('/yatra/v1')) {
1319 sessionUrl = `${restBase}/?rest_route=/yatra/v1/booking/session`;
1320 } else if (restBase.includes('/yatra/v1')) {
1321 sessionUrl = `${restBase}/booking/session`;
1322 } else {
1323 sessionUrl = `${restBase}/yatra/v1/booking/session`;
1324 }
1325
1326 fetch(sessionUrl, {
1327 method: 'POST',
1328 credentials: 'same-origin',
1329 headers: {
1330 'Content-Type': 'application/json',
1331 'X-WP-Nonce': nonce
1332 },
1333 body: JSON.stringify({
1334 additional_services: serviceIds
1335 })
1336 })
1337 .then(response => response.json())
1338 .then(response => {
1339 if (response.success) {
1340 schedulePricingSummaryRefresh();
1341 }
1342 })
1343 .catch(error => {
1344 console.error('Error updating services in session:', error);
1345 });
1346 }
1347 });
1348
1349 // Add CSS for spinner animation
1350 const style = document.createElement('style');
1351 style.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
1352 document.head.appendChild(style);
1353
1354 // =====================
1355 // Auth Forms (Login/Register)
1356 // =====================
1357
1358 // Tab switching
1359 $(document).on('click', '.yatra-auth-tab, .yatra-switch-tab', function() {
1360 const tab = $(this).data('tab');
1361
1362 // Update tabs
1363 $('.yatra-auth-tab').removeClass('active');
1364 $(`.yatra-auth-tab[data-tab="${tab}"]`).addClass('active');
1365
1366 // Update content
1367 $('.yatra-auth-content').hide();
1368 $(`#yatra-auth-${tab}`).show();
1369 });
1370
1371 // Toggle password visibility
1372 $(document).on('click', '.yatra-toggle-password', function() {
1373 const $input = $(this).parent().find('input');
1374 const $eyeOpen = $(this).find('.eye-open');
1375 const $eyeClosed = $(this).find('.eye-closed');
1376
1377 if ($input.attr('type') === 'password') {
1378 $input.attr('type', 'text');
1379 $eyeOpen.hide();
1380 $eyeClosed.show();
1381 } else {
1382 $input.attr('type', 'password');
1383 $eyeOpen.show();
1384 $eyeClosed.hide();
1385 }
1386 });
1387
1388 // Password strength indicator
1389 $(document).on('input', '#yatra-reg-password', function() {
1390 const password = $(this).val();
1391 const $strengthMeter = $('#yatra-password-strength');
1392 let strength = 0;
1393
1394 if (password.length >= 8) strength++;
1395 if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength++;
1396 if (password.match(/[0-9]/)) strength++;
1397 if (password.match(/[^a-zA-Z0-9]/)) strength++;
1398
1399 $strengthMeter.removeClass('weak fair good strong');
1400 if (password.length > 0) {
1401 if (strength <= 1) $strengthMeter.addClass('weak');
1402 else if (strength === 2) $strengthMeter.addClass('fair');
1403 else if (strength === 3) $strengthMeter.addClass('good');
1404 else $strengthMeter.addClass('strong');
1405 }
1406 });
1407
1408 // Login form submission via REST API
1409 $(document).on('submit', '#yatra-login-form', function(e) {
1410 e.preventDefault();
1411
1412 const $form = $(this);
1413 const $btn = $form.find('.yatra-auth-submit');
1414 const $btnText = $btn.find('.btn-text');
1415 const $btnLoading = $btn.find('.btn-loading');
1416 const $messageEl = $('#yatra-login-message');
1417
1418 // Remove any existing resend link
1419 $('.yatra-resend-verification').remove();
1420
1421 // Show loading
1422 $btn.prop('disabled', true);
1423 $btnText.hide();
1424 $btnLoading.css('display', 'flex');
1425 $messageEl.hide();
1426
1427 // Prepare data for REST API
1428 const loginData = {
1429 username: $('#yatra-login-email').val(),
1430 password: $('#yatra-login-password').val(),
1431 remember: $('input[name="rememberme"]').is(':checked')
1432 };
1433
1434 fetch(apiUrl + '/auth/login', {
1435 method: 'POST',
1436 headers: {
1437 'Content-Type': 'application/json',
1438 'X-WP-Nonce': nonce
1439 },
1440 credentials: 'same-origin',
1441 body: JSON.stringify(loginData)
1442 })
1443 .then(response => response.json())
1444 .then(response => {
1445 if (response.success) {
1446 $messageEl.removeClass('error').addClass('success');
1447 $messageEl.text(response.message || 'Login successful! Redirecting...');
1448 $messageEl.show();
1449
1450 // Reload page to show booking form
1451 setTimeout(function() {
1452 window.location.reload();
1453 }, 1000);
1454 } else {
1455 $messageEl.removeClass('success').addClass('error');
1456 $messageEl.html(response.message || 'Invalid credentials. Please try again.');
1457 $messageEl.show();
1458
1459 // If email needs verification, show resend option
1460 if (response.needs_verification && response.email) {
1461 const $resendLink = $('<div class="yatra-resend-verification" style="margin-top: 12px; text-align: center;">' +
1462 '<span style="color: #6b7280; font-size: 14px;">Didn\'t receive the email? </span>' +
1463 '<button type="button" class="yatra-resend-btn" data-email="' + response.email + '" style="background: none; border: none; color: #3b82f6; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: underline;">Resend verification link</button>' +
1464 '</div>');
1465 $messageEl.after($resendLink);
1466 }
1467
1468 $btn.prop('disabled', false);
1469 $btnText.show();
1470 $btnLoading.hide();
1471 }
1472 })
1473 .catch(function() {
1474 $messageEl.removeClass('success').addClass('error');
1475 $messageEl.text('An error occurred. Please try again.');
1476 $messageEl.show();
1477
1478 $btn.prop('disabled', false);
1479 $btnText.show();
1480 $btnLoading.hide();
1481 });
1482 });
1483
1484 // Resend verification email
1485 $(document).on('click', '.yatra-resend-btn', function(e) {
1486 e.preventDefault();
1487
1488 const $btn = $(this);
1489 const email = $btn.data('email');
1490 const $messageEl = $('#yatra-login-message');
1491
1492 // Don't allow click if countdown is active
1493 if ($btn.data('countdown-active')) {
1494 return;
1495 }
1496
1497 // Disable button and show loading
1498 $btn.prop('disabled', true).text('Sending...');
1499
1500 fetch(apiUrl + '/auth/resend-verification', {
1501 method: 'POST',
1502 headers: {
1503 'Content-Type': 'application/json',
1504 'X-WP-Nonce': nonce
1505 },
1506 credentials: 'same-origin',
1507 body: JSON.stringify({ email: email })
1508 })
1509 .then(response => response.json())
1510 .then(response => {
1511 if (response.success) {
1512 $messageEl.removeClass('error').addClass('success');
1513 $messageEl.text(response.message);
1514 $messageEl.show();
1515 $('.yatra-resend-verification').remove();
1516 } else if (response.rate_limited && response.remaining_seconds) {
1517 // Start countdown timer
1518 startResendCountdown($btn, response.remaining_seconds);
1519 $messageEl.removeClass('success').addClass('error');
1520 $messageEl.text('Please wait before requesting another email.');
1521 $messageEl.show();
1522 } else {
1523 $messageEl.removeClass('success').addClass('error');
1524 $messageEl.text(response.message);
1525 $messageEl.show();
1526 $btn.prop('disabled', false).text('Resend verification link');
1527 }
1528 })
1529 .catch(function() {
1530 $messageEl.removeClass('success').addClass('error');
1531 $messageEl.text('An error occurred. Please try again.');
1532 $messageEl.show();
1533 $btn.prop('disabled', false).text('Resend verification link');
1534 });
1535 });
1536
1537 // Countdown timer for resend button
1538 function startResendCountdown($btn, seconds) {
1539 $btn.data('countdown-active', true);
1540 $btn.prop('disabled', true);
1541
1542 const updateCountdown = () => {
1543 if (seconds <= 0) {
1544 $btn.data('countdown-active', false);
1545 $btn.prop('disabled', false);
1546 $btn.html('Resend verification link');
1547 return;
1548 }
1549
1550 const mins = Math.floor(seconds / 60);
1551 const secs = seconds % 60;
1552 const timeStr = mins > 0
1553 ? `${mins}:${secs.toString().padStart(2, '0')}`
1554 : `${secs}s`;
1555
1556 $btn.html(`<span style="color: #9ca3af;">Resend in </span><span style="color: #3b82f6; font-weight: 700;">${timeStr}</span>`);
1557
1558 seconds--;
1559 setTimeout(updateCountdown, 1000);
1560 };
1561
1562 updateCountdown();
1563 }
1564
1565 // Registration form submission via REST API
1566 $(document).on('submit', '#yatra-register-form', function(e) {
1567 e.preventDefault();
1568
1569 const $form = $(this);
1570 const $btn = $form.find('.yatra-auth-submit');
1571 const $btnText = $btn.find('.btn-text');
1572 const $btnLoading = $btn.find('.btn-loading');
1573 const $messageEl = $('#yatra-register-message');
1574
1575 // Validate passwords match
1576 const password = $('#yatra-reg-password').val();
1577 const confirmPassword = $('#yatra-reg-confirm-password').val();
1578
1579 if (password !== confirmPassword) {
1580 $messageEl.removeClass('success').addClass('error');
1581 $messageEl.text('Passwords do not match.');
1582 $messageEl.show();
1583 return;
1584 }
1585
1586 // Show loading
1587 $btn.prop('disabled', true);
1588 $btnText.hide();
1589 $btnLoading.css('display', 'flex');
1590 $messageEl.hide();
1591
1592 // Prepare data for REST API
1593 const registerData = {
1594 first_name: $('#yatra-reg-first-name').val(),
1595 last_name: $('#yatra-reg-last-name').val(),
1596 email: $('#yatra-reg-email').val(),
1597 phone: $('#yatra-reg-phone').val(),
1598 password: password,
1599 confirm_password: confirmPassword
1600 };
1601
1602 fetch(apiUrl + '/auth/register', {
1603 method: 'POST',
1604 headers: {
1605 'Content-Type': 'application/json',
1606 'X-WP-Nonce': nonce
1607 },
1608 credentials: 'same-origin',
1609 body: JSON.stringify(registerData)
1610 })
1611 .then(response => response.json())
1612 .then(response => {
1613 if (response.success) {
1614 $messageEl.removeClass('error').addClass('success');
1615 $messageEl.text(response.message || 'Account created! Please check your email to verify.');
1616 $messageEl.show();
1617
1618 // Reset form
1619 $form[0].reset();
1620 $('#yatra-password-strength').removeClass('weak fair good strong');
1621
1622 // If verification is required, switch to login tab after a delay
1623 if (response.require_verification) {
1624 $btn.prop('disabled', false);
1625 $btnText.show();
1626 $btnLoading.hide();
1627
1628 // Show message and switch to login tab after 3 seconds
1629 setTimeout(function() {
1630 // Switch to login tab
1631 $('.yatra-auth-tab').removeClass('active');
1632 $('.yatra-auth-tab[data-tab="login"]').addClass('active');
1633 $('.yatra-auth-content').hide();
1634 $('#yatra-auth-login').show();
1635
1636 // Show info message on login tab
1637 const $loginMessage = $('#yatra-login-message');
1638 $loginMessage.removeClass('error').addClass('success');
1639 $loginMessage.text('Please check your email and click the verification link, then login here.');
1640 $loginMessage.show();
1641 }, 3000);
1642 } else {
1643 // Reload page if no verification required (shouldn't happen normally)
1644 setTimeout(function() {
1645 window.location.reload();
1646 }, 1000);
1647 }
1648 } else {
1649 $messageEl.removeClass('success').addClass('error');
1650 $messageEl.text(response.message || 'Registration failed. Please try again.');
1651 $messageEl.show();
1652
1653 $btn.prop('disabled', false);
1654 $btnText.show();
1655 $btnLoading.hide();
1656 }
1657 })
1658 .catch(function() {
1659 $messageEl.removeClass('success').addClass('error');
1660 $messageEl.text('An error occurred. Please try again.');
1661 $messageEl.show();
1662
1663 $btn.prop('disabled', false);
1664 $btnText.show();
1665 $btnLoading.hide();
1666 });
1667 });
1668
1669 // End of document ready
1670 })(jQuery);
1671