PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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 / single-trip.js

single-trip.js in Yatra – Travel Booking & Tour Operator Software 3.0.8, at assets/js/single-trip.js

703 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Single Trip Page JavaScript
3 *
4 * Handles frontend interactions for the single trip page including:
5 * - Group discount calculations
6 * - Traveler count management
7 * - Pricing updates
8 * - Flatpickr initialization
9 *
10 * @package Yatra
11 */
12
13 // Translation helpers — idempotent shim matching booking.js / trip.js so
14 // `__()` resolves through wp.i18n when wp-i18n is enqueued, otherwise it
15 // falls back to the source string.
16 (function () {
17 if (typeof window.__ === 'function') return;
18 if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
19 window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
20 window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
21 window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
22 window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
23 } else {
24 window.__ = function (text) { return text; };
25 window._n = function (s, p, n) { return n === 1 ? s : p; };
26 window._x = function (text) { return text; };
27 window.sprintf = window.sprintf || function (fmt) { return fmt; };
28 }
29 })();
30
31 document.addEventListener('DOMContentLoaded', function() {
32 function yatraGetFlatpickrLocale() {
33 var l = window.yatraTripData && window.yatraTripData.flatpickrLocale;
34 if (!l || typeof l !== 'object') {
35 return { firstDayOfWeek: 1 };
36 }
37 var out = Object.assign({}, l);
38 if (typeof out.firstDayOfWeek !== 'number' || isNaN(out.firstDayOfWeek)) {
39 out.firstDayOfWeek = 1;
40 }
41 return out;
42 }
43
44 /**
45 * Convert a PHP date() format string to its Flatpickr equivalent.
46 *
47 * The two format spec tables overlap heavily — d/j/D/l/m/n/M/F/Y/y
48 * mean the same thing in both — but a few PHP tokens map to a
49 * different Flatpickr token (e.g. PHP `s` = seconds → Flatpickr
50 * `S`; PHP `a`/`A` AM-PM → Flatpickr `K`). Anything outside the
51 * map (literal punctuation, slashes, spaces) is passed through.
52 *
53 * Used to populate flatpickr's `altFormat`. We keep `dateFormat`
54 * itself locked to `Y-m-d` so the value submitted to the backend
55 * stays stable regardless of the operator's display preference;
56 * only the user-visible input swaps to their configured format.
57 *
58 * Kept in lockstep with the helper of the same shape in trip.js
59 * and the inline mapping in single-trip-sidebar.js — three call
60 * sites because each file ships as its own enqueued bundle and
61 * sharing across them would require a separate dep file.
62 *
63 * @param {string} php PHP date() format from Settings → General → Date format
64 * @returns {string} Flatpickr-compatible format string
65 */
66 function yatraPhpDateFormatToFlatpickr(php) {
67 if (!php || typeof php !== 'string') return 'F j, Y';
68 var map = {
69 // Day
70 d: 'd', j: 'j', D: 'D', l: 'l',
71 // Month
72 m: 'm', n: 'n', M: 'M', F: 'F',
73 // Year
74 Y: 'Y', y: 'y',
75 // Time
76 H: 'H', G: 'H', h: 'h', g: 'h',
77 i: 'i', s: 'S', a: 'K', A: 'K'
78 };
79 var out = '';
80 var esc = false;
81 for (var i = 0; i < php.length; i++) {
82 var ch = php[i];
83 if (esc) { out += ch; esc = false; continue; }
84 if (ch === '\\') { esc = true; continue; }
85 out += map[ch] || ch;
86 }
87 return out || 'F j, Y';
88 }
89
90 /**
91 * Resolve the operator's configured display format and convert it
92 * for Flatpickr. Falls back to `F j, Y` (e.g. "Jan 5, 2026") when
93 * settings haven't shipped a value.
94 */
95 function yatraResolveAltFormat() {
96 var cfg = window.yatraTripData || {};
97 // Pre-converted value wins (set server-side in some bundles); a
98 // raw PHP string is converted on the fly.
99 if (cfg.flatpickrAltFormat || cfg.altDateFormat) {
100 return cfg.flatpickrAltFormat || cfg.altDateFormat;
101 }
102 var php = cfg.dateFormat || cfg.date_format || '';
103 return yatraPhpDateFormatToFlatpickr(php);
104 }
105
106 var fpLocale = yatraGetFlatpickrLocale();
107 var fpAltFormat = yatraResolveAltFormat();
108
109 // Initialize date pickers when DOM is ready.
110 //
111 // Priority order applied here MUST mirror the server-side
112 // AvailabilityResolutionService:
113 // 1. Specific Availability Dates (per-date rows) ← highest
114 // 2. Recurring Rules (pattern-generated dates)
115 // 3. Trip default flexible window (available_from / available_to) ← lowest
116 //
117 // Server has already merged (1) and (2) into yatraTripData.availabilityDates.
118 // When the trip is in date-specific mode (or any availability-restricted
119 // mode the template might use) we lock the date picker to that merged list.
120 // Otherwise we fall back to the flexible window encoded in data-min-date /
121 // data-max-date.
122 const bookingDateInput = document.getElementById('travel_date');
123 if (bookingDateInput) {
124 const form = bookingDateInput.closest('.yatra-booking-form');
125 const bookingMode = form ? form.getAttribute('data-booking-mode') : 'regular';
126
127 // Accept any of the historic / current values that signal
128 // "restrict to specific availability dates". The PHP template now
129 // emits "date_specific"; older builds used "availability". Both
130 // must trigger the restricted picker so the priority chain holds.
131 const isDateSpecific = (
132 bookingMode === 'date_specific'
133 || bookingMode === 'availability'
134 || bookingMode === 'date-specific'
135 );
136
137 if (isDateSpecific) {
138 // Availability-based: only the dates resolved by the priority
139 // chain (specific dates first, then recurring rules) are pickable.
140 const availabilityDates = (window.yatraTripData && Array.isArray(window.yatraTripData.availabilityDates))
141 ? window.yatraTripData.availabilityDates
142 : [];
143
144 if (availabilityDates.length > 0) {
145 flatpickr(bookingDateInput, {
146 // Backend value stays as Y-m-d (what the booking
147 // form submits); only the user-visible input flips
148 // to the operator's configured Date Format via
149 // altInput/altFormat. Previously hardcoded which
150 // ignored Settings → General → Date format.
151 dateFormat: 'Y-m-d',
152 altInput: true,
153 altFormat: fpAltFormat,
154 minDate: 'today',
155 enable: availabilityDates,
156 disableMobile: false,
157 locale: fpLocale
158 });
159 } else {
160 // Edge case: template believes the trip is date-specific but
161 // the merged date list is empty (e.g. all rules expired).
162 // Disable the input rather than silently falling back to
163 // "any date", which would violate the configured priority.
164 bookingDateInput.setAttribute('disabled', 'disabled');
165 // Fallback placeholder when the template hasn't supplied
166 // `data-empty-text`. Wrapped in __() so the literal string
167 // can be translated; the data-attribute fallback is a
168 // server-rendered string that's already localised.
169 bookingDateInput.setAttribute(
170 'placeholder',
171 bookingDateInput.getAttribute('data-empty-text') || __('No departure dates available', 'yatra')
172 );
173 }
174 } else {
175 // Flexible booking: any future date inside the trip's available
176 // window (available_from / available_to are surfaced as
177 // data-min-date / data-max-date by the sidebar template).
178 const minDate = bookingDateInput.getAttribute('data-min-date') || 'today';
179 const maxDate = bookingDateInput.getAttribute('data-max-date');
180
181 // Flexible-date picker. Same altInput/altFormat pattern
182 // as the availability-restricted branch above — backend
183 // value stable, display matches operator preference.
184 const config = {
185 dateFormat: 'Y-m-d',
186 altInput: true,
187 altFormat: fpAltFormat,
188 minDate: minDate,
189 disableMobile: false,
190 locale: fpLocale
191 };
192
193 if (maxDate) {
194 config.maxDate = maxDate;
195 }
196
197 flatpickr(bookingDateInput, config);
198 }
199 }
200
201 // Enquiry modal date picker (always flexible). Same altFormat
202 // treatment so the enquiry form respects the operator's date
203 // format too — previously hardcoded to Y-m-d which jarred with
204 // any non-ISO display preference.
205 const enquiryDateInput = document.getElementById('enquiry-travel-date');
206 if (enquiryDateInput) {
207 flatpickr(enquiryDateInput, {
208 dateFormat: 'Y-m-d',
209 altInput: true,
210 altFormat: fpAltFormat,
211 minDate: 'today',
212 disableMobile: false,
213 locale: fpLocale
214 });
215 }
216 });
217
218 // Group Discount Calculation for Booking Widget
219 document.addEventListener('DOMContentLoaded', function () {
220 const bookingForm = document.querySelector('.yatra-booking-form');
221 const totalAmountElement = document.getElementById('total-amount');
222 const travelerInputs = document.querySelectorAll('input[name*="travelers["], input[name="num_travelers"]');
223 const dateInput = document.getElementById('travel_date');
224 let currentGroupDiscounts = null;
225 let selectedAvailability = null;
226
227 // Load group discounts for this trip
228 async function loadGroupDiscounts() {
229 try {
230 const response = await fetch(window.yatraTripData?.groupDiscountsUrl || '', {
231 method: 'GET',
232 headers: {
233 'Content-Type': 'application/json',
234 },
235 body: JSON.stringify({
236 trip_ids: [window.yatraTripData?.tripId]
237 })
238 });
239
240 if (response.ok) {
241 const data = await response.json();
242 if (data[window.yatraTripData?.tripId] && data[window.yatraTripData?.tripId].has_group_discounts) {
243 currentGroupDiscounts = data[window.yatraTripData?.tripId].discounts;
244 }
245 }
246 } catch (error) {
247 console.warn('Failed to load group discounts:', error);
248 }
249 }
250
251 // Calculate total travelers
252 function calculateTotalTravelers() {
253 let total = 0;
254
255 // Handle traveler-based pricing (multiple categories)
256 const travelerCategoryInputs = document.querySelectorAll('input[name*="travelers["]');
257 travelerCategoryInputs.forEach(input => {
258 total += parseInt(input.value) || 0;
259 });
260
261 // Handle simple pricing (single traveler count)
262 const simpleTravelerInput = document.querySelector('input[name="num_travelers"]');
263 if (simpleTravelerInput) {
264 total = parseInt(simpleTravelerInput.value) || 0;
265 }
266
267 return total;
268 }
269
270 // Calculate group discount for current traveler count
271 function calculateGroupDiscount(totalTravelers, basePrice) {
272 if (!currentGroupDiscounts || totalTravelers < 2) {
273 return 0;
274 }
275
276 // Find applicable discount for this group size
277 let applicableDiscount = null;
278 for (const discount of currentGroupDiscounts) {
279 const minSize = discount.min_group_size;
280 const maxSize = discount.max_group_size;
281
282 if (totalTravelers >= minSize && (!maxSize || totalTravelers <= maxSize)) {
283 applicableDiscount = discount;
284 break;
285 }
286 }
287
288 if (!applicableDiscount) {
289 return 0;
290 }
291
292 let discountAmount = 0;
293
294 if (applicableDiscount.discount_mode === 'category_based' && applicableDiscount.category_discounts) {
295 // Calculate category-based discount
296 const travelerCounts = getTravelerCountsByCategory();
297
298 for (const [category, discountConfig] of Object.entries(applicableDiscount.category_discounts)) {
299 const count = travelerCounts[category] || 0;
300 if (count > 0) {
301 // Calculate category-specific price
302 const categoryMultiplier = getCategoryMultiplier(category);
303 const categoryPrice = basePrice * categoryMultiplier;
304 const categoryTotal = categoryPrice * count;
305
306 if (discountConfig.type === 'percentage') {
307 discountAmount += categoryTotal * (discountConfig.amount / 100);
308 } else {
309 // Fixed amount per person
310 discountAmount += Math.min(discountConfig.amount * count, categoryTotal);
311 }
312 }
313 }
314 } else {
315 // Total-based discount
316 if (applicableDiscount.discount_type === 'percentage') {
317 discountAmount = basePrice * totalTravelers * (applicableDiscount.discount_amount / 100);
318 } else {
319 discountAmount = Math.min(applicableDiscount.discount_amount, basePrice * totalTravelers);
320 }
321 }
322
323 return discountAmount;
324 }
325
326 function getSidebarAvailabilities() {
327 const raw = window.yatraTripData && window.yatraTripData.sidebarAvailability;
328 return Array.isArray(raw) ? raw : [];
329 }
330
331 // Update pricing when date changes
332 if (dateInput) {
333 dateInput.addEventListener('change', function() {
334 const selectedDate = this.value;
335 const availabilities = getSidebarAvailabilities();
336 if (availabilities.length > 0) {
337 selectedAvailability = availabilities.find(a => a.departure_date === selectedDate);
338
339 if (selectedAvailability && selectedAvailability.pricing_type === 'traveler_based' && selectedAvailability.price_types) {
340 selectedAvailability.price_types.forEach((pt) => {
341 const input = document.querySelector(`input[name="travelers[${pt.category_id}]"]`);
342 if (input) {
343 const price = pt.discounted_price || pt.original_price || 0;
344 input.setAttribute('data-price', price);
345 }
346 });
347 }
348 } else {
349 selectedAvailability = null;
350 }
351
352 updatePricing();
353 });
354
355 // Set today's date by default if available, otherwise use trip fallback pricing
356 const today = new Date().toISOString().split('T')[0];
357 if (!dateInput.value) {
358 const availabilities = getSidebarAvailabilities();
359 if (availabilities.length > 0) {
360 const todayAvailability = availabilities.find(a => a.departure_date === today);
361 if (todayAvailability) {
362 dateInput.value = today;
363 dateInput.dispatchEvent(new Event('change'));
364 } else {
365 updatePricing();
366 }
367 }
368 }
369 }
370
371 // Update pricing display
372 function updatePricing() {
373 const totalTravelers = calculateTotalTravelers();
374 let basePrice = window.yatraTripData?.basePrice || 0;
375
376 // Calculate subtotal
377 let subtotal = 0;
378
379 const categoryInputs = document.querySelectorAll('input[name*="travelers["]');
380 if (categoryInputs.length > 0) {
381 // Traveler-based pricing: read actual prices from data attributes (updated by date change)
382 categoryInputs.forEach(input => {
383 const count = parseInt(input.value) || 0;
384 const price = parseFloat(input.dataset.price) || 0;
385 const pricingMode = input.dataset.pricingMode || 'per_person';
386 const groupOverflow = input.dataset.groupOverflow || 'block';
387 const maxPax = parseInt(input.dataset.maxPax) || 0;
388
389 if (pricingMode === 'per_group') {
390 // Per group: one flat price, or per block of maxPax people
391 // when overflow is "per_block".
392 if (count > 0) {
393 subtotal += (groupOverflow === 'per_block' && maxPax > 0)
394 ? price * Math.ceil(count / maxPax)
395 : price;
396 }
397 } else {
398 // Per person: charge per traveler
399 subtotal += price * count;
400 }
401 });
402 } else {
403 // Simple pricing - use selected availability price if available
404 if (selectedAvailability && selectedAvailability.price) {
405 basePrice = parseFloat(selectedAvailability.price);
406 }
407 subtotal = basePrice * totalTravelers;
408 }
409
410 // Calculate group discount
411 const groupDiscount = calculateGroupDiscount(totalTravelers, basePrice);
412 const finalTotal = subtotal - groupDiscount;
413
414 // Update display (total element has been removed from sidebar)
415 // The total price display is no longer shown in the sidebar
416 // This code is kept for reference but will not execute as totalAmountElement is null
417 if (totalAmountElement) {
418 totalAmountElement.textContent = (window.yatraTripData?.currencySymbol || '$') + finalTotal.toFixed(2);
419 }
420
421 // Show/hide group discount indicator
422 updateGroupDiscountIndicator(totalTravelers, groupDiscount);
423 }
424
425 // Update group discount indicator (total element has been removed)
426 function updateGroupDiscountIndicator(totalTravelers, discountAmount) {
427 let indicator = document.querySelector('.yatra-group-discount-indicator');
428
429 if (discountAmount > 0) {
430 if (!indicator) {
431 indicator = document.createElement('div');
432 indicator.className = 'yatra-group-discount-indicator';
433 // Find a suitable parent to append the indicator
434 const bookingForm = document.querySelector('.yatra-booking-form');
435 if (bookingForm) {
436 bookingForm.appendChild(indicator);
437 }
438 }
439
440 indicator.innerHTML = `
441 <div class="yatra-group-discount-badge">
442 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
443 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
444 </svg>
445 Group discount applied: -${window.yatraTripData?.currencySymbol || '$'}${discountAmount.toFixed(2)}
446 </div>
447 `;
448 } else if (indicator) {
449 indicator.remove();
450 }
451 }
452
453 // Initialize
454 if (window.yatraTripData) {
455 loadGroupDiscounts().then(() => {
456 // Set up event listeners for traveler count changes
457 travelerInputs.forEach(input => {
458 input.addEventListener('change', updatePricing);
459 input.addEventListener('input', updatePricing);
460 });
461
462 // Also listen for plus/minus button clicks
463 document.addEventListener('click', function(e) {
464 if (e.target.closest('.yatra-quantity-plus') || e.target.closest('.yatra-quantity-minus')) {
465 // Delay to ensure input value is updated first
466 setTimeout(updatePricing, 50);
467 }
468 });
469
470 // Initial pricing update
471 updatePricing();
472 });
473
474 // Handle group discount CTA buttons
475 document.addEventListener('click', function (e) {
476 if (e.target.classList.contains('yatra-group-discount-btn')) {
477 const groupSize = parseInt(e.target.dataset.groupSize);
478 if (groupSize) {
479 // Set traveler count to trigger discount
480 if (document.querySelector('input[name="num_travelers"]')) {
481 // Simple pricing
482 document.querySelector('input[name="num_travelers"]').value = groupSize;
483 } else {
484 // Traveler-based pricing - set adults to group size
485 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
486 if (adultInput) {
487 adultInput.value = groupSize;
488 // Update display. The literal "Adult x %d"
489 // is kept as the msgid so translators can
490 // localise both the noun and the format
491 // (some locales put the multiplier first).
492 // Comment phrasing matches the other call
493 // sites in trip.js so WP-CLI sees ONE
494 // canonical translators-comment per msgid.
495 const display = document.querySelector('.yatra-participants-display');
496 if (display) {
497 // translators: %d: number of adults
498 display.textContent = __('Adult x %d', 'yatra').replace('%d', String(groupSize));
499 }
500 }
501 }
502
503 // Trigger pricing update
504 updatePricing();
505
506 // Scroll to booking widget
507 document.getElementById('yatra-booking-widget').scrollIntoView({
508 behavior: 'smooth',
509 block: 'center'
510 });
511 }
512 }
513 });
514 }
515
516 /**
517 * Traveler Synchronization Across All Locations
518 * Syncs traveler inputs between sidebar, enquiry modal, and availability section
519 */
520
521 // Handle +/- button clicks for traveler inputs
522 document.addEventListener('click', function(e) {
523 // Check if click is on a quantity button or its children (SVG, path, etc.)
524 const btn = e.target.closest('.yatra-quantity-btn, .yatra-quantity-plus, .yatra-quantity-minus');
525 if (!btn) return;
526
527 // Prevent default button behavior
528 e.preventDefault();
529 e.stopPropagation();
530
531 const target = btn.getAttribute('data-target');
532 if (!target) return;
533
534 // Try to find input by ID first (for simple inputs)
535 let input = document.getElementById(target);
536
537 // If not found by ID, find by data-target in the same row (for category inputs)
538 if (!input) {
539 const row = btn.closest('.yatra-quantity-row');
540 if (row) {
541 input = row.querySelector('input[type="number"]');
542 }
543 }
544
545 if (!input) return;
546
547 const min = parseInt(input.getAttribute('min')) || 0;
548 const max = parseInt(input.getAttribute('max')) || 99;
549 let value = parseInt(input.value) || 0;
550
551 // Update value based on button type
552 if (btn.classList.contains('yatra-quantity-plus')) {
553 value = Math.min(value + 1, max);
554 } else if (btn.classList.contains('yatra-quantity-minus')) {
555 value = Math.max(value - 1, min);
556 }
557
558 // Update the input
559 input.value = value;
560
561 // Update button states
562 const minusBtn = btn.classList.contains('yatra-quantity-minus') ? btn : btn.parentElement.querySelector('.yatra-quantity-minus');
563 const plusBtn = btn.classList.contains('yatra-quantity-plus') ? btn : btn.parentElement.querySelector('.yatra-quantity-plus');
564
565 if (minusBtn) minusBtn.disabled = value <= min;
566 if (plusBtn) plusBtn.disabled = value >= max;
567
568 // Trigger change event
569 input.dispatchEvent(new Event('change', { bubbles: true }));
570
571 // Sync to other locations
572 syncTravelerInputs(input);
573 });
574
575 // Handle direct input changes
576 document.addEventListener('change', function(e) {
577 const input = e.target;
578
579 // Check if it's a traveler input
580 if (input.classList.contains('yatra-quantity-input') ||
581 input.classList.contains('yatra-quantity-input-simple') ||
582 input.id === 'num_travelers' ||
583 input.id === 'enquiry_adults' ||
584 input.name?.startsWith('travelers[') ||
585 input.id?.startsWith('traveler_')) {
586
587 syncTravelerInputs(input);
588 }
589 });
590
591 /**
592 * Sync traveler input value to all matching inputs across locations
593 */
594 function syncTravelerInputs(sourceInput) {
595 const sourceId = sourceInput.id;
596 const sourceName = sourceInput.name;
597 const sourceCategory = sourceInput.getAttribute('data-category');
598 const sourceValue = parseInt(sourceInput.value) || 0;
599
600 // Determine if this is regular pricing or traveler-based pricing
601 if (sourceId === 'num_travelers' || sourceId === 'enquiry_adults' || sourceId?.startsWith('num-travelers-')) {
602 // Regular pricing - sync all num_travelers and enquiry_adults inputs
603 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
604 regularInputs.forEach(input => {
605 if (input !== sourceInput) {
606 const max = parseInt(input.getAttribute('max')) || 99;
607 input.value = Math.min(sourceValue, max);
608 }
609 });
610
611 // Also sync to availability section inputs
612 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
613 availabilityInputs.forEach(input => {
614 if (input !== sourceInput) {
615 const max = parseInt(input.getAttribute('max')) || 99;
616 input.value = Math.min(sourceValue, max);
617 }
618 });
619
620 } else if (sourceId?.startsWith('traveler_') || sourceName?.startsWith('travelers[') || sourceCategory) {
621 // Traveler-based pricing - extract category ID
622 let categoryId = '';
623
624 if (sourceCategory) {
625 // From availability section (data-category attribute)
626 categoryId = sourceCategory;
627 } else if (sourceId?.startsWith('traveler_')) {
628 categoryId = sourceId.replace('traveler_', '');
629 } else if (sourceName?.includes('[')) {
630 const match = sourceName.match(/travelers\[(\d+)\]/);
631 if (match) categoryId = match[1];
632 }
633
634 if (categoryId) {
635 // Sync all inputs for this category across all locations
636 const categoryInputs = document.querySelectorAll(
637 `input[id="traveler_${categoryId}"], input[name="travelers[${categoryId}]"], input[data-category="${categoryId}"]`
638 );
639
640 categoryInputs.forEach(input => {
641 if (input !== sourceInput) {
642 const max = parseInt(input.getAttribute('max')) || 99;
643 input.value = Math.min(sourceValue, max);
644
645 // Update button states for this input
646 const row = input.closest('.yatra-quantity-row');
647 if (row) {
648 const min = parseInt(input.getAttribute('min')) || 0;
649 const minusBtn = row.querySelector('.yatra-quantity-minus');
650 const plusBtn = row.querySelector('.yatra-quantity-plus');
651 if (minusBtn) minusBtn.disabled = input.value <= min;
652 if (plusBtn) plusBtn.disabled = input.value >= max;
653 }
654 }
655 });
656
657 // Update dropdown display text if exists
658 updateTravelerDisplayText();
659 }
660 }
661 }
662
663 /**
664 * Update traveler dropdown display text
665 */
666 function updateTravelerDisplayText() {
667 const displays = document.querySelectorAll('.yatra-traveler-selector-display, .yatra-participants-display');
668
669 displays.forEach(display => {
670 // Find all traveler inputs in the same container
671 const container = display.closest('.yatra-traveler-selector, .yatra-booking-field-select');
672 if (!container) return;
673
674 const dropdown = container.querySelector('.yatra-traveler-selector-dropdown, .yatra-booking-quantity-selector');
675 if (!dropdown) return;
676
677 const inputs = dropdown.querySelectorAll('input[type="number"]');
678 const parts = [];
679 let total = 0;
680
681 inputs.forEach(input => {
682 const value = parseInt(input.value) || 0;
683 if (value > 0) {
684 const label = input.getAttribute('data-category-label') || __('Traveler', 'yatra');
685 parts.push(sprintf(
686 /* translators: 1: traveler category label, 2: count. */
687 __('%1$s x %2$d', 'yatra'),
688 label,
689 value
690 ));
691 total += value;
692 }
693 });
694
695 if (parts.length > 0) {
696 display.textContent = parts.join(', ');
697 } else {
698 display.textContent = __('Select travelers', 'yatra');
699 }
700 });
701 }
702 });
703