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

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