/** * Single Trip Page JavaScript * * Handles frontend interactions for the single trip page including: * - Group discount calculations * - Traveler count management * - Pricing updates * - Flatpickr initialization * * @package Yatra */ // Translation helpers — idempotent shim matching booking.js / trip.js so // `__()` resolves through wp.i18n when wp-i18n is enqueued, otherwise it // falls back to the source string. (function () { if (typeof window.__ === 'function') return; if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') { window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); }; window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); }; window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); }; window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; }); } else { window.__ = function (text) { return text; }; window._n = function (s, p, n) { return n === 1 ? s : p; }; window._x = function (text) { return text; }; window.sprintf = window.sprintf || function (fmt) { return fmt; }; } })(); document.addEventListener('DOMContentLoaded', function() { function yatraGetFlatpickrLocale() { var l = window.yatraTripData && window.yatraTripData.flatpickrLocale; if (!l || typeof l !== 'object') { return { firstDayOfWeek: 1 }; } var out = Object.assign({}, l); if (typeof out.firstDayOfWeek !== 'number' || isNaN(out.firstDayOfWeek)) { out.firstDayOfWeek = 1; } return out; } /** * Convert a PHP date() format string to its Flatpickr equivalent. * * The two format spec tables overlap heavily — d/j/D/l/m/n/M/F/Y/y * mean the same thing in both — but a few PHP tokens map to a * different Flatpickr token (e.g. PHP `s` = seconds → Flatpickr * `S`; PHP `a`/`A` AM-PM → Flatpickr `K`). Anything outside the * map (literal punctuation, slashes, spaces) is passed through. * * Used to populate flatpickr's `altFormat`. We keep `dateFormat` * itself locked to `Y-m-d` so the value submitted to the backend * stays stable regardless of the operator's display preference; * only the user-visible input swaps to their configured format. * * Kept in lockstep with the helper of the same shape in trip.js * and the inline mapping in single-trip-sidebar.js — three call * sites because each file ships as its own enqueued bundle and * sharing across them would require a separate dep file. * * @param {string} php PHP date() format from Settings → General → Date format * @returns {string} Flatpickr-compatible format string */ function yatraPhpDateFormatToFlatpickr(php) { if (!php || typeof php !== 'string') return 'F j, Y'; var map = { // Day d: 'd', j: 'j', D: 'D', l: 'l', // Month m: 'm', n: 'n', M: 'M', F: 'F', // Year Y: 'Y', y: 'y', // Time H: 'H', G: 'H', h: 'h', g: 'h', i: 'i', s: 'S', a: 'K', A: 'K' }; var out = ''; var esc = false; for (var i = 0; i < php.length; i++) { var ch = php[i]; if (esc) { out += ch; esc = false; continue; } if (ch === '\\') { esc = true; continue; } out += map[ch] || ch; } return out || 'F j, Y'; } /** * Resolve the operator's configured display format and convert it * for Flatpickr. Falls back to `F j, Y` (e.g. "Jan 5, 2026") when * settings haven't shipped a value. */ function yatraResolveAltFormat() { var cfg = window.yatraTripData || {}; // Pre-converted value wins (set server-side in some bundles); a // raw PHP string is converted on the fly. if (cfg.flatpickrAltFormat || cfg.altDateFormat) { return cfg.flatpickrAltFormat || cfg.altDateFormat; } var php = cfg.dateFormat || cfg.date_format || ''; return yatraPhpDateFormatToFlatpickr(php); } var fpLocale = yatraGetFlatpickrLocale(); var fpAltFormat = yatraResolveAltFormat(); // Initialize date pickers when DOM is ready. // // Priority order applied here MUST mirror the server-side // AvailabilityResolutionService: // 1. Specific Availability Dates (per-date rows) ← highest // 2. Recurring Rules (pattern-generated dates) // 3. Trip default flexible window (available_from / available_to) ← lowest // // Server has already merged (1) and (2) into yatraTripData.availabilityDates. // When the trip is in date-specific mode (or any availability-restricted // mode the template might use) we lock the date picker to that merged list. // Otherwise we fall back to the flexible window encoded in data-min-date / // data-max-date. const bookingDateInput = document.getElementById('travel_date'); if (bookingDateInput) { const form = bookingDateInput.closest('.yatra-booking-form'); const bookingMode = form ? form.getAttribute('data-booking-mode') : 'regular'; // Accept any of the historic / current values that signal // "restrict to specific availability dates". The PHP template now // emits "date_specific"; older builds used "availability". Both // must trigger the restricted picker so the priority chain holds. const isDateSpecific = ( bookingMode === 'date_specific' || bookingMode === 'availability' || bookingMode === 'date-specific' ); if (isDateSpecific) { // Availability-based: only the dates resolved by the priority // chain (specific dates first, then recurring rules) are pickable. const availabilityDates = (window.yatraTripData && Array.isArray(window.yatraTripData.availabilityDates)) ? window.yatraTripData.availabilityDates : []; if (availabilityDates.length > 0) { flatpickr(bookingDateInput, { // Backend value stays as Y-m-d (what the booking // form submits); only the user-visible input flips // to the operator's configured Date Format via // altInput/altFormat. Previously hardcoded which // ignored Settings → General → Date format. dateFormat: 'Y-m-d', altInput: true, altFormat: fpAltFormat, minDate: 'today', enable: availabilityDates, disableMobile: false, locale: fpLocale }); } else { // Edge case: template believes the trip is date-specific but // the merged date list is empty (e.g. all rules expired). // Disable the input rather than silently falling back to // "any date", which would violate the configured priority. bookingDateInput.setAttribute('disabled', 'disabled'); // Fallback placeholder when the template hasn't supplied // `data-empty-text`. Wrapped in __() so the literal string // can be translated; the data-attribute fallback is a // server-rendered string that's already localised. bookingDateInput.setAttribute( 'placeholder', bookingDateInput.getAttribute('data-empty-text') || __('No departure dates available', 'yatra') ); } } else { // Flexible booking: any future date inside the trip's available // window (available_from / available_to are surfaced as // data-min-date / data-max-date by the sidebar template). const minDate = bookingDateInput.getAttribute('data-min-date') || 'today'; const maxDate = bookingDateInput.getAttribute('data-max-date'); // Flexible-date picker. Same altInput/altFormat pattern // as the availability-restricted branch above — backend // value stable, display matches operator preference. const config = { dateFormat: 'Y-m-d', altInput: true, altFormat: fpAltFormat, minDate: minDate, disableMobile: false, locale: fpLocale }; if (maxDate) { config.maxDate = maxDate; } flatpickr(bookingDateInput, config); } } // Enquiry modal date picker (always flexible). Same altFormat // treatment so the enquiry form respects the operator's date // format too — previously hardcoded to Y-m-d which jarred with // any non-ISO display preference. const enquiryDateInput = document.getElementById('enquiry-travel-date'); if (enquiryDateInput) { flatpickr(enquiryDateInput, { dateFormat: 'Y-m-d', altInput: true, altFormat: fpAltFormat, minDate: 'today', disableMobile: false, locale: fpLocale }); } }); // Group Discount Calculation for Booking Widget document.addEventListener('DOMContentLoaded', function () { const bookingForm = document.querySelector('.yatra-booking-form'); const totalAmountElement = document.getElementById('total-amount'); const travelerInputs = document.querySelectorAll('input[name*="travelers["], input[name="num_travelers"]'); const dateInput = document.getElementById('travel_date'); let currentGroupDiscounts = null; let selectedAvailability = null; // Load group discounts for this trip async function loadGroupDiscounts() { try { const response = await fetch(window.yatraTripData?.groupDiscountsUrl || '', { method: 'GET', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ trip_ids: [window.yatraTripData?.tripId] }) }); if (response.ok) { const data = await response.json(); if (data[window.yatraTripData?.tripId] && data[window.yatraTripData?.tripId].has_group_discounts) { currentGroupDiscounts = data[window.yatraTripData?.tripId].discounts; } } } catch (error) { console.warn('Failed to load group discounts:', error); } } // Calculate total travelers function calculateTotalTravelers() { let total = 0; // Handle traveler-based pricing (multiple categories) const travelerCategoryInputs = document.querySelectorAll('input[name*="travelers["]'); travelerCategoryInputs.forEach(input => { total += parseInt(input.value) || 0; }); // Handle simple pricing (single traveler count) const simpleTravelerInput = document.querySelector('input[name="num_travelers"]'); if (simpleTravelerInput) { total = parseInt(simpleTravelerInput.value) || 0; } return total; } // Calculate group discount for current traveler count function calculateGroupDiscount(totalTravelers, basePrice) { if (!currentGroupDiscounts || totalTravelers < 2) { return 0; } // Find applicable discount for this group size let applicableDiscount = null; for (const discount of currentGroupDiscounts) { const minSize = discount.min_group_size; const maxSize = discount.max_group_size; if (totalTravelers >= minSize && (!maxSize || totalTravelers <= maxSize)) { applicableDiscount = discount; break; } } if (!applicableDiscount) { return 0; } let discountAmount = 0; if (applicableDiscount.discount_mode === 'category_based' && applicableDiscount.category_discounts) { // Calculate category-based discount const travelerCounts = getTravelerCountsByCategory(); for (const [category, discountConfig] of Object.entries(applicableDiscount.category_discounts)) { const count = travelerCounts[category] || 0; if (count > 0) { // Calculate category-specific price const categoryMultiplier = getCategoryMultiplier(category); const categoryPrice = basePrice * categoryMultiplier; const categoryTotal = categoryPrice * count; if (discountConfig.type === 'percentage') { discountAmount += categoryTotal * (discountConfig.amount / 100); } else { // Fixed amount per person discountAmount += Math.min(discountConfig.amount * count, categoryTotal); } } } } else { // Total-based discount if (applicableDiscount.discount_type === 'percentage') { discountAmount = basePrice * totalTravelers * (applicableDiscount.discount_amount / 100); } else { discountAmount = Math.min(applicableDiscount.discount_amount, basePrice * totalTravelers); } } return discountAmount; } function getSidebarAvailabilities() { const raw = window.yatraTripData && window.yatraTripData.sidebarAvailability; return Array.isArray(raw) ? raw : []; } // Update pricing when date changes if (dateInput) { dateInput.addEventListener('change', function() { const selectedDate = this.value; const availabilities = getSidebarAvailabilities(); if (availabilities.length > 0) { selectedAvailability = availabilities.find(a => a.departure_date === selectedDate); if (selectedAvailability && selectedAvailability.pricing_type === 'traveler_based' && selectedAvailability.price_types) { selectedAvailability.price_types.forEach((pt) => { const input = document.querySelector(`input[name="travelers[${pt.category_id}]"]`); if (input) { const price = pt.discounted_price || pt.original_price || 0; input.setAttribute('data-price', price); } }); } } else { selectedAvailability = null; } updatePricing(); }); // Set today's date by default if available, otherwise use trip fallback pricing const today = new Date().toISOString().split('T')[0]; if (!dateInput.value) { const availabilities = getSidebarAvailabilities(); if (availabilities.length > 0) { const todayAvailability = availabilities.find(a => a.departure_date === today); if (todayAvailability) { dateInput.value = today; dateInput.dispatchEvent(new Event('change')); } else { updatePricing(); } } } } // Update pricing display function updatePricing() { const totalTravelers = calculateTotalTravelers(); let basePrice = window.yatraTripData?.basePrice || 0; // Calculate subtotal let subtotal = 0; const categoryInputs = document.querySelectorAll('input[name*="travelers["]'); if (categoryInputs.length > 0) { // Traveler-based pricing: read actual prices from data attributes (updated by date change) categoryInputs.forEach(input => { const count = parseInt(input.value) || 0; const price = parseFloat(input.dataset.price) || 0; const pricingMode = input.dataset.pricingMode || 'per_person'; if (pricingMode === 'per_group') { // Per group: charge flat price once if any travelers if (count > 0) { subtotal += price; } } else { // Per person: charge per traveler subtotal += price * count; } }); } else { // Simple pricing - use selected availability price if available if (selectedAvailability && selectedAvailability.price) { basePrice = parseFloat(selectedAvailability.price); } subtotal = basePrice * totalTravelers; } // Calculate group discount const groupDiscount = calculateGroupDiscount(totalTravelers, basePrice); const finalTotal = subtotal - groupDiscount; // Update display (total element has been removed from sidebar) // The total price display is no longer shown in the sidebar // This code is kept for reference but will not execute as totalAmountElement is null if (totalAmountElement) { totalAmountElement.textContent = (window.yatraTripData?.currencySymbol || '$') + finalTotal.toFixed(2); } // Show/hide group discount indicator updateGroupDiscountIndicator(totalTravelers, groupDiscount); } // Update group discount indicator (total element has been removed) function updateGroupDiscountIndicator(totalTravelers, discountAmount) { let indicator = document.querySelector('.yatra-group-discount-indicator'); if (discountAmount > 0) { if (!indicator) { indicator = document.createElement('div'); indicator.className = 'yatra-group-discount-indicator'; // Find a suitable parent to append the indicator const bookingForm = document.querySelector('.yatra-booking-form'); if (bookingForm) { bookingForm.appendChild(indicator); } } indicator.innerHTML = `