PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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.6, at assets/js/single-trip.js

698 lines 29.8 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
387 if (pricingMode === 'per_group') {
388 // Per group: charge flat price once if any travelers
389 if (count > 0) {
390 subtotal += price;
391 }
392 } else {
393 // Per person: charge per traveler
394 subtotal += price * count;
395 }
396 });
397 } else {
398 // Simple pricing - use selected availability price if available
399 if (selectedAvailability && selectedAvailability.price) {
400 basePrice = parseFloat(selectedAvailability.price);
401 }
402 subtotal = basePrice * totalTravelers;
403 }
404
405 // Calculate group discount
406 const groupDiscount = calculateGroupDiscount(totalTravelers, basePrice);
407 const finalTotal = subtotal - groupDiscount;
408
409 // Update display (total element has been removed from sidebar)
410 // The total price display is no longer shown in the sidebar
411 // This code is kept for reference but will not execute as totalAmountElement is null
412 if (totalAmountElement) {
413 totalAmountElement.textContent = (window.yatraTripData?.currencySymbol || '$') + finalTotal.toFixed(2);
414 }
415
416 // Show/hide group discount indicator
417 updateGroupDiscountIndicator(totalTravelers, groupDiscount);
418 }
419
420 // Update group discount indicator (total element has been removed)
421 function updateGroupDiscountIndicator(totalTravelers, discountAmount) {
422 let indicator = document.querySelector('.yatra-group-discount-indicator');
423
424 if (discountAmount > 0) {
425 if (!indicator) {
426 indicator = document.createElement('div');
427 indicator.className = 'yatra-group-discount-indicator';
428 // Find a suitable parent to append the indicator
429 const bookingForm = document.querySelector('.yatra-booking-form');
430 if (bookingForm) {
431 bookingForm.appendChild(indicator);
432 }
433 }
434
435 indicator.innerHTML = `
436 <div class="yatra-group-discount-badge">
437 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
438 <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"/>
439 </svg>
440 Group discount applied: -${window.yatraTripData?.currencySymbol || '$'}${discountAmount.toFixed(2)}
441 </div>
442 `;
443 } else if (indicator) {
444 indicator.remove();
445 }
446 }
447
448 // Initialize
449 if (window.yatraTripData) {
450 loadGroupDiscounts().then(() => {
451 // Set up event listeners for traveler count changes
452 travelerInputs.forEach(input => {
453 input.addEventListener('change', updatePricing);
454 input.addEventListener('input', updatePricing);
455 });
456
457 // Also listen for plus/minus button clicks
458 document.addEventListener('click', function(e) {
459 if (e.target.closest('.yatra-quantity-plus') || e.target.closest('.yatra-quantity-minus')) {
460 // Delay to ensure input value is updated first
461 setTimeout(updatePricing, 50);
462 }
463 });
464
465 // Initial pricing update
466 updatePricing();
467 });
468
469 // Handle group discount CTA buttons
470 document.addEventListener('click', function (e) {
471 if (e.target.classList.contains('yatra-group-discount-btn')) {
472 const groupSize = parseInt(e.target.dataset.groupSize);
473 if (groupSize) {
474 // Set traveler count to trigger discount
475 if (document.querySelector('input[name="num_travelers"]')) {
476 // Simple pricing
477 document.querySelector('input[name="num_travelers"]').value = groupSize;
478 } else {
479 // Traveler-based pricing - set adults to group size
480 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
481 if (adultInput) {
482 adultInput.value = groupSize;
483 // Update display. The literal "Adult x %d"
484 // is kept as the msgid so translators can
485 // localise both the noun and the format
486 // (some locales put the multiplier first).
487 // Comment phrasing matches the other call
488 // sites in trip.js so WP-CLI sees ONE
489 // canonical translators-comment per msgid.
490 const display = document.querySelector('.yatra-participants-display');
491 if (display) {
492 // translators: %d: number of adults
493 display.textContent = __('Adult x %d', 'yatra').replace('%d', String(groupSize));
494 }
495 }
496 }
497
498 // Trigger pricing update
499 updatePricing();
500
501 // Scroll to booking widget
502 document.getElementById('yatra-booking-widget').scrollIntoView({
503 behavior: 'smooth',
504 block: 'center'
505 });
506 }
507 }
508 });
509 }
510
511 /**
512 * Traveler Synchronization Across All Locations
513 * Syncs traveler inputs between sidebar, enquiry modal, and availability section
514 */
515
516 // Handle +/- button clicks for traveler inputs
517 document.addEventListener('click', function(e) {
518 // Check if click is on a quantity button or its children (SVG, path, etc.)
519 const btn = e.target.closest('.yatra-quantity-btn, .yatra-quantity-plus, .yatra-quantity-minus');
520 if (!btn) return;
521
522 // Prevent default button behavior
523 e.preventDefault();
524 e.stopPropagation();
525
526 const target = btn.getAttribute('data-target');
527 if (!target) return;
528
529 // Try to find input by ID first (for simple inputs)
530 let input = document.getElementById(target);
531
532 // If not found by ID, find by data-target in the same row (for category inputs)
533 if (!input) {
534 const row = btn.closest('.yatra-quantity-row');
535 if (row) {
536 input = row.querySelector('input[type="number"]');
537 }
538 }
539
540 if (!input) return;
541
542 const min = parseInt(input.getAttribute('min')) || 0;
543 const max = parseInt(input.getAttribute('max')) || 99;
544 let value = parseInt(input.value) || 0;
545
546 // Update value based on button type
547 if (btn.classList.contains('yatra-quantity-plus')) {
548 value = Math.min(value + 1, max);
549 } else if (btn.classList.contains('yatra-quantity-minus')) {
550 value = Math.max(value - 1, min);
551 }
552
553 // Update the input
554 input.value = value;
555
556 // Update button states
557 const minusBtn = btn.classList.contains('yatra-quantity-minus') ? btn : btn.parentElement.querySelector('.yatra-quantity-minus');
558 const plusBtn = btn.classList.contains('yatra-quantity-plus') ? btn : btn.parentElement.querySelector('.yatra-quantity-plus');
559
560 if (minusBtn) minusBtn.disabled = value <= min;
561 if (plusBtn) plusBtn.disabled = value >= max;
562
563 // Trigger change event
564 input.dispatchEvent(new Event('change', { bubbles: true }));
565
566 // Sync to other locations
567 syncTravelerInputs(input);
568 });
569
570 // Handle direct input changes
571 document.addEventListener('change', function(e) {
572 const input = e.target;
573
574 // Check if it's a traveler input
575 if (input.classList.contains('yatra-quantity-input') ||
576 input.classList.contains('yatra-quantity-input-simple') ||
577 input.id === 'num_travelers' ||
578 input.id === 'enquiry_adults' ||
579 input.name?.startsWith('travelers[') ||
580 input.id?.startsWith('traveler_')) {
581
582 syncTravelerInputs(input);
583 }
584 });
585
586 /**
587 * Sync traveler input value to all matching inputs across locations
588 */
589 function syncTravelerInputs(sourceInput) {
590 const sourceId = sourceInput.id;
591 const sourceName = sourceInput.name;
592 const sourceCategory = sourceInput.getAttribute('data-category');
593 const sourceValue = parseInt(sourceInput.value) || 0;
594
595 // Determine if this is regular pricing or traveler-based pricing
596 if (sourceId === 'num_travelers' || sourceId === 'enquiry_adults' || sourceId?.startsWith('num-travelers-')) {
597 // Regular pricing - sync all num_travelers and enquiry_adults inputs
598 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
599 regularInputs.forEach(input => {
600 if (input !== sourceInput) {
601 const max = parseInt(input.getAttribute('max')) || 99;
602 input.value = Math.min(sourceValue, max);
603 }
604 });
605
606 // Also sync to availability section inputs
607 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
608 availabilityInputs.forEach(input => {
609 if (input !== sourceInput) {
610 const max = parseInt(input.getAttribute('max')) || 99;
611 input.value = Math.min(sourceValue, max);
612 }
613 });
614
615 } else if (sourceId?.startsWith('traveler_') || sourceName?.startsWith('travelers[') || sourceCategory) {
616 // Traveler-based pricing - extract category ID
617 let categoryId = '';
618
619 if (sourceCategory) {
620 // From availability section (data-category attribute)
621 categoryId = sourceCategory;
622 } else if (sourceId?.startsWith('traveler_')) {
623 categoryId = sourceId.replace('traveler_', '');
624 } else if (sourceName?.includes('[')) {
625 const match = sourceName.match(/travelers\[(\d+)\]/);
626 if (match) categoryId = match[1];
627 }
628
629 if (categoryId) {
630 // Sync all inputs for this category across all locations
631 const categoryInputs = document.querySelectorAll(
632 `input[id="traveler_${categoryId}"], input[name="travelers[${categoryId}]"], input[data-category="${categoryId}"]`
633 );
634
635 categoryInputs.forEach(input => {
636 if (input !== sourceInput) {
637 const max = parseInt(input.getAttribute('max')) || 99;
638 input.value = Math.min(sourceValue, max);
639
640 // Update button states for this input
641 const row = input.closest('.yatra-quantity-row');
642 if (row) {
643 const min = parseInt(input.getAttribute('min')) || 0;
644 const minusBtn = row.querySelector('.yatra-quantity-minus');
645 const plusBtn = row.querySelector('.yatra-quantity-plus');
646 if (minusBtn) minusBtn.disabled = input.value <= min;
647 if (plusBtn) plusBtn.disabled = input.value >= max;
648 }
649 }
650 });
651
652 // Update dropdown display text if exists
653 updateTravelerDisplayText();
654 }
655 }
656 }
657
658 /**
659 * Update traveler dropdown display text
660 */
661 function updateTravelerDisplayText() {
662 const displays = document.querySelectorAll('.yatra-traveler-selector-display, .yatra-participants-display');
663
664 displays.forEach(display => {
665 // Find all traveler inputs in the same container
666 const container = display.closest('.yatra-traveler-selector, .yatra-booking-field-select');
667 if (!container) return;
668
669 const dropdown = container.querySelector('.yatra-traveler-selector-dropdown, .yatra-booking-quantity-selector');
670 if (!dropdown) return;
671
672 const inputs = dropdown.querySelectorAll('input[type="number"]');
673 const parts = [];
674 let total = 0;
675
676 inputs.forEach(input => {
677 const value = parseInt(input.value) || 0;
678 if (value > 0) {
679 const label = input.getAttribute('data-category-label') || __('Traveler', 'yatra');
680 parts.push(sprintf(
681 /* translators: 1: traveler category label, 2: count. */
682 __('%1$s x %2$d', 'yatra'),
683 label,
684 value
685 ));
686 total += value;
687 }
688 });
689
690 if (parts.length > 0) {
691 display.textContent = parts.join(', ');
692 } else {
693 display.textContent = __('Select travelers', 'yatra');
694 }
695 });
696 }
697 });
698