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

542 lines 22.6 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 document.addEventListener('DOMContentLoaded', function() {
14 // Initialize date pickers when DOM is ready
15 const bookingDateInput = document.getElementById('travel_date');
16 if (bookingDateInput) {
17 const form = bookingDateInput.closest('.yatra-booking-form');
18 const bookingMode = form ? form.getAttribute('data-booking-mode') : 'regular';
19
20 if (bookingMode === 'availability') {
21 // Availability-based: Only specific dates
22 const availabilityDates = window.yatraTripData?.availabilityDates || [];
23
24 if (availabilityDates.length > 0) {
25 flatpickr(bookingDateInput, {
26 dateFormat: 'Y-m-d',
27 minDate: 'today',
28 enable: availabilityDates,
29 disableMobile: false
30 });
31 }
32 } else {
33 // Flexible booking: Any future date with constraints
34 const minDate = bookingDateInput.getAttribute('data-min-date') || 'today';
35 const maxDate = bookingDateInput.getAttribute('data-max-date');
36
37 const config = {
38 dateFormat: 'Y-m-d',
39 minDate: minDate,
40 disableMobile: false
41 };
42
43 if (maxDate) {
44 config.maxDate = maxDate;
45 }
46
47 flatpickr(bookingDateInput, config);
48 }
49 }
50
51 // Enquiry modal date picker (always flexible)
52 const enquiryDateInput = document.getElementById('enquiry-travel-date');
53 if (enquiryDateInput) {
54 flatpickr(enquiryDateInput, {
55 dateFormat: 'Y-m-d',
56 minDate: 'today',
57 disableMobile: false
58 });
59 }
60 });
61
62 // Group Discount Calculation for Booking Widget
63 document.addEventListener('DOMContentLoaded', function () {
64 const bookingForm = document.querySelector('.yatra-booking-form');
65 const totalAmountElement = document.getElementById('total-amount');
66 const travelerInputs = document.querySelectorAll('input[name*="travelers["], input[name="num_travelers"]');
67 const dateInput = document.getElementById('travel_date');
68 let currentGroupDiscounts = null;
69 let selectedAvailability = null;
70
71 // Load group discounts for this trip
72 async function loadGroupDiscounts() {
73 try {
74 const response = await fetch(window.yatraTripData?.groupDiscountsUrl || '', {
75 method: 'GET',
76 headers: {
77 'Content-Type': 'application/json',
78 },
79 body: JSON.stringify({
80 trip_ids: [window.yatraTripData?.tripId]
81 })
82 });
83
84 if (response.ok) {
85 const data = await response.json();
86 if (data[window.yatraTripData?.tripId] && data[window.yatraTripData?.tripId].has_group_discounts) {
87 currentGroupDiscounts = data[window.yatraTripData?.tripId].discounts;
88 }
89 }
90 } catch (error) {
91 console.warn('Failed to load group discounts:', error);
92 }
93 }
94
95 // Calculate total travelers
96 function calculateTotalTravelers() {
97 let total = 0;
98
99 // Handle traveler-based pricing (multiple categories)
100 const travelerCategoryInputs = document.querySelectorAll('input[name*="travelers["]');
101 travelerCategoryInputs.forEach(input => {
102 total += parseInt(input.value) || 0;
103 });
104
105 // Handle simple pricing (single traveler count)
106 const simpleTravelerInput = document.querySelector('input[name="num_travelers"]');
107 if (simpleTravelerInput) {
108 total = parseInt(simpleTravelerInput.value) || 0;
109 }
110
111 return total;
112 }
113
114 // Calculate group discount for current traveler count
115 function calculateGroupDiscount(totalTravelers, basePrice) {
116 if (!currentGroupDiscounts || totalTravelers < 2) {
117 return 0;
118 }
119
120 // Find applicable discount for this group size
121 let applicableDiscount = null;
122 for (const discount of currentGroupDiscounts) {
123 const minSize = discount.min_group_size;
124 const maxSize = discount.max_group_size;
125
126 if (totalTravelers >= minSize && (!maxSize || totalTravelers <= maxSize)) {
127 applicableDiscount = discount;
128 break;
129 }
130 }
131
132 if (!applicableDiscount) {
133 return 0;
134 }
135
136 let discountAmount = 0;
137
138 if (applicableDiscount.discount_mode === 'category_based' && applicableDiscount.category_discounts) {
139 // Calculate category-based discount
140 const travelerCounts = getTravelerCountsByCategory();
141
142 for (const [category, discountConfig] of Object.entries(applicableDiscount.category_discounts)) {
143 const count = travelerCounts[category] || 0;
144 if (count > 0) {
145 // Calculate category-specific price
146 const categoryMultiplier = getCategoryMultiplier(category);
147 const categoryPrice = basePrice * categoryMultiplier;
148 const categoryTotal = categoryPrice * count;
149
150 if (discountConfig.type === 'percentage') {
151 discountAmount += categoryTotal * (discountConfig.amount / 100);
152 } else {
153 // Fixed amount per person
154 discountAmount += Math.min(discountConfig.amount * count, categoryTotal);
155 }
156 }
157 }
158 } else {
159 // Total-based discount
160 if (applicableDiscount.discount_type === 'percentage') {
161 discountAmount = basePrice * totalTravelers * (applicableDiscount.discount_amount / 100);
162 } else {
163 discountAmount = Math.min(applicableDiscount.discount_amount, basePrice * totalTravelers);
164 }
165 }
166
167 return discountAmount;
168 }
169
170 // Update pricing when date changes
171 if (dateInput) {
172 dateInput.addEventListener('change', function() {
173 const selectedDate = this.value;
174 const bookingCard = document.querySelector('.yatra-booking-card');
175
176 if (bookingCard) {
177 const availabilityData = bookingCard.getAttribute('data-availability');
178 if (availabilityData) {
179 try {
180 const availabilities = JSON.parse(availabilityData);
181 selectedAvailability = availabilities.find(a => a.departure_date === selectedDate);
182
183 // Update traveler input prices if traveler-based pricing
184 if (selectedAvailability && selectedAvailability.pricing_type === 'traveler_based' && selectedAvailability.price_types) {
185 selectedAvailability.price_types.forEach((pt, index) => {
186 const input = document.querySelector(`input[name="travelers[${pt.category_id}]"]`);
187 if (input) {
188 const price = pt.discounted_price || pt.original_price || 0;
189 input.setAttribute('data-price', price);
190 }
191 });
192 }
193
194 // Recalculate total
195 updatePricing();
196 } catch (e) {
197 console.error('Error parsing availability data:', e);
198 }
199 }
200 }
201 });
202
203 // Set today's date by default if available, otherwise use trip fallback pricing
204 const today = new Date().toISOString().split('T')[0];
205 const bookingCard = document.querySelector('.yatra-booking-card');
206 if (bookingCard && !dateInput.value) {
207 const availabilityData = bookingCard.getAttribute('data-availability');
208 if (availabilityData) {
209 try {
210 const availabilities = JSON.parse(availabilityData);
211 const todayAvailability = availabilities.find(a => a.departure_date === today);
212 if (todayAvailability) {
213 dateInput.value = today;
214 dateInput.dispatchEvent(new Event('change'));
215 } else {
216 // No availability for today - use trip's default pricing (already set in HTML)
217 // Just trigger initial calculation with trip prices
218 updatePricing();
219 }
220 } catch (e) {
221 console.error('Error setting default date:', e);
222 }
223 }
224 }
225 }
226
227 // Update pricing display
228 function updatePricing() {
229 const totalTravelers = calculateTotalTravelers();
230 let basePrice = window.yatraTripData?.basePrice || 0;
231
232 // Calculate subtotal
233 let subtotal = 0;
234
235 const categoryInputs = document.querySelectorAll('input[name*="travelers["]');
236 if (categoryInputs.length > 0) {
237 // Traveler-based pricing: read actual prices from data attributes (updated by date change)
238 categoryInputs.forEach(input => {
239 const count = parseInt(input.value) || 0;
240 const price = parseFloat(input.dataset.price) || 0;
241 const pricingMode = input.dataset.pricingMode || 'per_person';
242
243 if (pricingMode === 'per_group') {
244 // Per group: charge flat price once if any travelers
245 if (count > 0) {
246 subtotal += price;
247 }
248 } else {
249 // Per person: charge per traveler
250 subtotal += price * count;
251 }
252 });
253 } else {
254 // Simple pricing - use selected availability price if available
255 if (selectedAvailability && selectedAvailability.price) {
256 basePrice = parseFloat(selectedAvailability.price);
257 }
258 subtotal = basePrice * totalTravelers;
259 }
260
261 // Calculate group discount
262 const groupDiscount = calculateGroupDiscount(totalTravelers, basePrice);
263 const finalTotal = subtotal - groupDiscount;
264
265 // Update display (total element has been removed from sidebar)
266 // The total price display is no longer shown in the sidebar
267 // This code is kept for reference but will not execute as totalAmountElement is null
268 if (totalAmountElement) {
269 totalAmountElement.textContent = (window.yatraTripData?.currencySymbol || '$') + finalTotal.toFixed(2);
270 }
271
272 // Show/hide group discount indicator
273 updateGroupDiscountIndicator(totalTravelers, groupDiscount);
274 }
275
276 // Update group discount indicator (total element has been removed)
277 function updateGroupDiscountIndicator(totalTravelers, discountAmount) {
278 let indicator = document.querySelector('.yatra-group-discount-indicator');
279
280 if (discountAmount > 0) {
281 if (!indicator) {
282 indicator = document.createElement('div');
283 indicator.className = 'yatra-group-discount-indicator';
284 // Find a suitable parent to append the indicator
285 const bookingForm = document.querySelector('.yatra-booking-form');
286 if (bookingForm) {
287 bookingForm.appendChild(indicator);
288 }
289 }
290
291 indicator.innerHTML = `
292 <div class="yatra-group-discount-badge">
293 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
294 <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"/>
295 </svg>
296 Group discount applied: -${window.yatraTripData?.currencySymbol || '$'}${discountAmount.toFixed(2)}
297 </div>
298 `;
299 } else if (indicator) {
300 indicator.remove();
301 }
302 }
303
304 // Initialize
305 if (window.yatraTripData) {
306 loadGroupDiscounts().then(() => {
307 // Set up event listeners for traveler count changes
308 travelerInputs.forEach(input => {
309 input.addEventListener('change', updatePricing);
310 input.addEventListener('input', updatePricing);
311 });
312
313 // Also listen for plus/minus button clicks
314 document.addEventListener('click', function(e) {
315 if (e.target.closest('.yatra-quantity-plus') || e.target.closest('.yatra-quantity-minus')) {
316 // Delay to ensure input value is updated first
317 setTimeout(updatePricing, 50);
318 }
319 });
320
321 // Initial pricing update
322 updatePricing();
323 });
324
325 // Handle group discount CTA buttons
326 document.addEventListener('click', function (e) {
327 if (e.target.classList.contains('yatra-group-discount-btn')) {
328 const groupSize = parseInt(e.target.dataset.groupSize);
329 if (groupSize) {
330 // Set traveler count to trigger discount
331 if (document.querySelector('input[name="num_travelers"]')) {
332 // Simple pricing
333 document.querySelector('input[name="num_travelers"]').value = groupSize;
334 } else {
335 // Traveler-based pricing - set adults to group size
336 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
337 if (adultInput) {
338 adultInput.value = groupSize;
339 // Update display
340 const display = document.querySelector('.yatra-participants-display');
341 if (display) {
342 display.textContent = `Adult x ${groupSize}`;
343 }
344 }
345 }
346
347 // Trigger pricing update
348 updatePricing();
349
350 // Scroll to booking widget
351 document.getElementById('yatra-booking-widget').scrollIntoView({
352 behavior: 'smooth',
353 block: 'center'
354 });
355 }
356 }
357 });
358 }
359
360 /**
361 * Traveler Synchronization Across All Locations
362 * Syncs traveler inputs between sidebar, enquiry modal, and availability section
363 */
364
365 // Handle +/- button clicks for traveler inputs
366 document.addEventListener('click', function(e) {
367 // Check if click is on a quantity button or its children (SVG, path, etc.)
368 const btn = e.target.closest('.yatra-quantity-btn, .yatra-quantity-plus, .yatra-quantity-minus');
369 if (!btn) return;
370
371 // Prevent default button behavior
372 e.preventDefault();
373 e.stopPropagation();
374
375 const target = btn.getAttribute('data-target');
376 if (!target) return;
377
378 // Try to find input by ID first (for simple inputs)
379 let input = document.getElementById(target);
380
381 // If not found by ID, find by data-target in the same row (for category inputs)
382 if (!input) {
383 const row = btn.closest('.yatra-quantity-row');
384 if (row) {
385 input = row.querySelector('input[type="number"]');
386 }
387 }
388
389 if (!input) return;
390
391 const min = parseInt(input.getAttribute('min')) || 0;
392 const max = parseInt(input.getAttribute('max')) || 99;
393 let value = parseInt(input.value) || 0;
394
395 // Update value based on button type
396 if (btn.classList.contains('yatra-quantity-plus')) {
397 value = Math.min(value + 1, max);
398 } else if (btn.classList.contains('yatra-quantity-minus')) {
399 value = Math.max(value - 1, min);
400 }
401
402 // Update the input
403 input.value = value;
404
405 // Update button states
406 const minusBtn = btn.classList.contains('yatra-quantity-minus') ? btn : btn.parentElement.querySelector('.yatra-quantity-minus');
407 const plusBtn = btn.classList.contains('yatra-quantity-plus') ? btn : btn.parentElement.querySelector('.yatra-quantity-plus');
408
409 if (minusBtn) minusBtn.disabled = value <= min;
410 if (plusBtn) plusBtn.disabled = value >= max;
411
412 // Trigger change event
413 input.dispatchEvent(new Event('change', { bubbles: true }));
414
415 // Sync to other locations
416 syncTravelerInputs(input);
417 });
418
419 // Handle direct input changes
420 document.addEventListener('change', function(e) {
421 const input = e.target;
422
423 // Check if it's a traveler input
424 if (input.classList.contains('yatra-quantity-input') ||
425 input.classList.contains('yatra-quantity-input-simple') ||
426 input.id === 'num_travelers' ||
427 input.id === 'enquiry_adults' ||
428 input.name?.startsWith('travelers[') ||
429 input.id?.startsWith('traveler_')) {
430
431 syncTravelerInputs(input);
432 }
433 });
434
435 /**
436 * Sync traveler input value to all matching inputs across locations
437 */
438 function syncTravelerInputs(sourceInput) {
439 const sourceId = sourceInput.id;
440 const sourceName = sourceInput.name;
441 const sourceCategory = sourceInput.getAttribute('data-category');
442 const sourceValue = parseInt(sourceInput.value) || 0;
443
444 // Determine if this is regular pricing or traveler-based pricing
445 if (sourceId === 'num_travelers' || sourceId === 'enquiry_adults' || sourceId?.startsWith('num-travelers-')) {
446 // Regular pricing - sync all num_travelers and enquiry_adults inputs
447 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
448 regularInputs.forEach(input => {
449 if (input !== sourceInput) {
450 const max = parseInt(input.getAttribute('max')) || 99;
451 input.value = Math.min(sourceValue, max);
452 }
453 });
454
455 // Also sync to availability section inputs
456 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
457 availabilityInputs.forEach(input => {
458 if (input !== sourceInput) {
459 const max = parseInt(input.getAttribute('max')) || 99;
460 input.value = Math.min(sourceValue, max);
461 }
462 });
463
464 } else if (sourceId?.startsWith('traveler_') || sourceName?.startsWith('travelers[') || sourceCategory) {
465 // Traveler-based pricing - extract category ID
466 let categoryId = '';
467
468 if (sourceCategory) {
469 // From availability section (data-category attribute)
470 categoryId = sourceCategory;
471 } else if (sourceId?.startsWith('traveler_')) {
472 categoryId = sourceId.replace('traveler_', '');
473 } else if (sourceName?.includes('[')) {
474 const match = sourceName.match(/travelers\[(\d+)\]/);
475 if (match) categoryId = match[1];
476 }
477
478 if (categoryId) {
479 // Sync all inputs for this category across all locations
480 const categoryInputs = document.querySelectorAll(
481 `input[id="traveler_${categoryId}"], input[name="travelers[${categoryId}]"], input[data-category="${categoryId}"]`
482 );
483
484 categoryInputs.forEach(input => {
485 if (input !== sourceInput) {
486 const max = parseInt(input.getAttribute('max')) || 99;
487 input.value = Math.min(sourceValue, max);
488
489 // Update button states for this input
490 const row = input.closest('.yatra-quantity-row');
491 if (row) {
492 const min = parseInt(input.getAttribute('min')) || 0;
493 const minusBtn = row.querySelector('.yatra-quantity-minus');
494 const plusBtn = row.querySelector('.yatra-quantity-plus');
495 if (minusBtn) minusBtn.disabled = input.value <= min;
496 if (plusBtn) plusBtn.disabled = input.value >= max;
497 }
498 }
499 });
500
501 // Update dropdown display text if exists
502 updateTravelerDisplayText();
503 }
504 }
505 }
506
507 /**
508 * Update traveler dropdown display text
509 */
510 function updateTravelerDisplayText() {
511 const displays = document.querySelectorAll('.yatra-traveler-selector-display, .yatra-participants-display');
512
513 displays.forEach(display => {
514 // Find all traveler inputs in the same container
515 const container = display.closest('.yatra-traveler-selector, .yatra-booking-field-select');
516 if (!container) return;
517
518 const dropdown = container.querySelector('.yatra-traveler-selector-dropdown, .yatra-booking-quantity-selector');
519 if (!dropdown) return;
520
521 const inputs = dropdown.querySelectorAll('input[type="number"]');
522 const parts = [];
523 let total = 0;
524
525 inputs.forEach(input => {
526 const value = parseInt(input.value) || 0;
527 if (value > 0) {
528 const label = input.getAttribute('data-category-label') || 'Traveler';
529 parts.push(`${label} x ${value}`);
530 total += value;
531 }
532 });
533
534 if (parts.length > 0) {
535 display.textContent = parts.join(', ');
536 } else {
537 display.textContent = 'Select travelers';
538 }
539 });
540 }
541 });
542