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

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