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 / templates / partials / single-trip / sticky-sidebar.php

sticky-sidebar.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.6, at templates/partials/single-trip/sticky-sidebar.php

561 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Mobile Sticky Sidebar Template
4 *
5 * Displays booking card in sticky bottom position on mobile devices
6 * SEO-friendly approach - separate template, no content duplication
7 *
8 * @package Yatra
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 // Expected variables: $trip, $has_availability, $has_traveler_pricing, $base_price, $pricing_type
16
17 // Determine if this is a multi-day trip
18 $is_multi_day = ($trip->getDurationDays() ?? 1) > 1;
19
20 // Check for group discounts availability (premium feature)
21 $sidebar_has_group_discounts = false;
22 $sidebar_group_discounts_data = [];
23 try {
24 if (class_exists('\Yatra\Services\DiscountService') &&
25 method_exists('\Yatra\Services\DiscountService', 'getGroupDiscountsForTrip')) {
26 $discountService = new \Yatra\Services\DiscountService();
27 $groupDiscountsResult = $discountService->getGroupDiscountsForTrip((int) $trip->getId());
28 if (!empty($groupDiscountsResult)) {
29 $sidebar_has_group_discounts = true;
30 $sidebar_group_discounts_data = $groupDiscountsResult;
31 }
32 }
33 } catch (\Exception $e) {
34 $sidebar_has_group_discounts = false;
35 }
36
37 // Prepare availability data for JavaScript
38 $availability_json = [];
39 $booking_mode = $trip->getBookingMode();
40 if ($has_availability) {
41 foreach ($trip->getAvailabilityDates() as $avail) {
42 $availability_json[] = [
43 'id' => (int) $avail->id,
44 'date' => $avail->departure_date,
45 'departure_date' => $avail->departure_date,
46 'return_date' => (isset($avail->return_date) && $avail->return_date !== '')
47 ? $avail->return_date
48 : (isset($avail->arrival_date) ? $avail->arrival_date : null),
49 'price' => $avail->effective_price ?? $avail->original_price,
50 'original_price' => $avail->original_price,
51 'discounted_price' => $avail->discounted_price,
52 'seats_available' => $avail->seats_available,
53 'seats_total' => $avail->seats_total,
54 'status' => $avail->status,
55 'is_limited' => $avail->is_limited ?? false,
56 'is_sold_out' => $avail->is_sold_out ?? false,
57 ];
58 }
59 }
60
61 // Calculate pricing - use pre-computed values from SingleTripController
62 $pricing = [
63 'has_price' => false,
64 'current_price' => '',
65 'original_price' => '',
66 'price_prefix' => '',
67 'has_discount' => false,
68 'raw_current_price' => 0,
69 'raw_original_price' => 0,
70 'is_traveler_based' => $has_traveler_pricing
71 ];
72
73 $discount = [
74 'has_discount' => false,
75 'discount_text' => '',
76 'discount_percentage' => 0
77 ];
78
79 // Use effective_price_min computed in SingleTripController
80 $effective_min = (float) ($trip->effective_price_min ?? 0);
81 $original_min = (float) ($trip->min_category_original_price ?? 0);
82 $max_discount_pct = (int) ($trip->max_discount_percentage ?? 0);
83
84 if ($effective_min > 0) {
85 $pricing['has_price'] = true;
86 $pricing['raw_current_price'] = $effective_min;
87 $pricing['current_price'] = yatra_format_price($effective_min);
88
89 if ($has_traveler_pricing || $has_availability) {
90 $pricing['price_prefix'] = __('From ', 'yatra');
91 }
92
93 if ($max_discount_pct > 0 && $original_min > $effective_min) {
94 $pricing['has_discount'] = true;
95 $pricing['raw_original_price'] = $original_min;
96 $pricing['original_price'] = yatra_format_price($original_min);
97
98 $discount['has_discount'] = true;
99 $discount['discount_percentage'] = $max_discount_pct;
100 $discount['discount_text'] = sprintf(__('Up to %d%%', 'yatra'), $max_discount_pct);
101 }
102 } else {
103 $original = (float) ($trip->getOriginalPrice() ?? 0);
104 $discounted = (float) ($trip->discounted_price ?? 0);
105
106 if ($discounted > 0) {
107 $current = $discounted;
108 } elseif ($original > 0) {
109 $current = $original;
110 } elseif (!empty($trip->getSalePrice()) && (float)$trip->getSalePrice() > 0) {
111 $current = (float) $trip->getSalePrice();
112 } else {
113 $current = 0;
114 }
115
116 if ($current > 0) {
117 $pricing['has_price'] = true;
118 $pricing['raw_current_price'] = $current;
119 $pricing['current_price'] = yatra_format_price($current);
120
121 if ($current < $original && $original > 0) {
122 $pct = round((($original - $current) / $original) * 100);
123
124 // Only show discount if percentage is greater than 0
125 if ($pct > 0) {
126 $pricing['has_discount'] = true;
127 $pricing['raw_original_price'] = $original;
128 $pricing['original_price'] = yatra_format_price($original);
129
130 $discount['has_discount'] = true;
131 $discount['discount_percentage'] = $pct;
132 $discount['discount_text'] = sprintf(__('%d%%', 'yatra'), $pct);
133 }
134 }
135 }
136 }
137 ?>
138 <div class="yatra-mobile-sticky-sidebar" id="yatra-mobile-sticky-sidebar"
139 data-has-availability="<?php echo $has_availability ? 'true' : 'false'; ?>"
140 data-booking-mode="<?php echo esc_attr($booking_mode); ?>"
141 data-is-multi-day="<?php echo $is_multi_day ? 'true' : 'false'; ?>"
142 data-pricing-type="<?php echo esc_attr($pricing_type); ?>"
143 data-availability='<?php echo esc_attr(json_encode($availability_json)); ?>'
144 data-group-discounts='<?php echo esc_attr(json_encode($sidebar_group_discounts_data)); ?>'>
145 <!-- Two-Row Layout -->
146 <div class="yatra-mobile-sticky-content">
147
148 <!-- Row 1: Date, Travelers, and Actions -->
149 <div class="yatra-mobile-row-1">
150 <!-- Group Discount Badge -->
151 <?php if ($sidebar_has_group_discounts): ?>
152 <div class="yatra-mobile-group-badge">
153 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
154 <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
155 <circle cx="9" cy="7" r="4"></circle>
156 <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
157 <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
158 </svg>
159 </div>
160 <?php endif; ?>
161
162 <!-- Date Picker - Compact Mobile Version -->
163 <div class="yatra-mobile-date-section">
164 <div class="yatra-mobile-compact-datepicker">
165 <div class="yatra-mobile-field-icon">
166 <?php echo yatra_svg_icon('calendar', 'yatra-mobile-icon'); ?>
167 </div>
168 <input type="text"
169 id="mobile_travel_date"
170 name="travel_date"
171 class="yatra-mobile-datepicker-input yatra-mobile-datepicker"
172 placeholder="<?php esc_attr_e('Date', 'yatra'); ?>"
173 data-min-date="<?php echo esc_attr($trip->getAvailableFrom() ?: date('Y-m-d')); ?>"
174 data-max-date="<?php echo esc_attr($trip->getAvailableTo() ?: ''); ?>"
175 readonly
176 required>
177 <svg class="yatra-mobile-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
178 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
179 </svg>
180 </div>
181 </div>
182
183 <!-- Travelers Selection -->
184 <div class="yatra-mobile-travelers-section">
185 <?php if ($has_traveler_pricing): ?>
186 <!-- Traveler-Based Pricing -->
187 <div class="yatra-mobile-booking-field">
188 <div class="yatra-mobile-field-icon">
189 <?php echo yatra_svg_icon('users', 'yatra-mobile-icon'); ?>
190 </div>
191 <div class="yatra-mobile-travelers-display" id="mobile-travelers-display">
192 <?php
193 $first_label = !empty($trip->price_types[0]->category_label)
194 ? $trip->price_types[0]->category_label
195 : __('Traveler', 'yatra');
196 echo esc_html($first_label . ' x 1');
197 ?>
198 </div>
199 </div>
200 <?php else: ?>
201 <!-- Regular Pricing -->
202 <div class="yatra-mobile-booking-field">
203 <div class="yatra-mobile-field-icon">
204 <?php echo yatra_svg_icon('users', 'yatra-mobile-icon'); ?>
205 </div>
206 <div class="yatra-mobile-quantity-controls">
207 <button type="button" class="yatra-mobile-quantity-btn yatra-mobile-quantity-minus" data-target="mobile_num_travelers">
208 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
209 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
210 </svg>
211 </button>
212 <input type="number"
213 id="mobile_num_travelers"
214 name="num_travelers"
215 class="yatra-mobile-quantity-input"
216 value="1"
217 min="<?php echo esc_attr($trip->getMinTravelers() ?: 1); ?>"
218 max="<?php echo esc_attr($trip->getMaxTravelers() ?: 20); ?>"
219 readonly
220 data-price="<?php echo esc_attr($base_price); ?>">
221 <button type="button" class="yatra-mobile-quantity-btn yatra-mobile-quantity-plus" data-target="mobile_num_travelers">
222 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
223 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
224 </svg>
225 </button>
226 </div>
227 </div>
228 <?php endif; ?>
229 </div>
230
231 <!-- Action Buttons -->
232 <div class="yatra-mobile-action-section">
233 <button type="button" class="yatra-mobile-check-btn" id="mobile-check-availability-btn" data-trip-id="<?php echo esc_attr($trip->getId()); ?>">
234 <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
235 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
236 </svg>
237 <span><?php echo esc_html__('Check', 'yatra'); ?></span>
238 </button>
239 <button type="button" class="yatra-mobile-enquire-btn" id="mobile-open-enquiry-modal" onclick="if (window.enquiryModal) { window.enquiryModal.open(); } else if (window.YatraEnquiry) { window.YatraEnquiry.open(); } return false;">
240 <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
241 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
242 </svg>
243 <span><?php echo esc_html__('Enquire', 'yatra'); ?></span>
244 </button>
245 </div>
246
247 <!-- Close Button -->
248 <div class="yatra-mobile-action-buttons">
249 <button type="button" class="yatra-mobile-close-btn" onclick="yatraCloseMobileSidebar()">
250 <?php echo yatra_svg_icon('x', 'yatra-mobile-close-icon'); ?>
251 </button>
252 </div>
253 </div>
254
255 </div>
256
257 </div>
258
259 <!-- JavaScript for mobile sidebar interactions -->
260 <script>
261 function yatraScrollToBooking() {
262 const bookingForm = document.querySelector('.yatra-booking-form');
263 if (bookingForm) {
264 bookingForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
265 } else {
266 // Fallback to main sidebar
267 const sidebar = document.querySelector('.yatra-trip-sidebar');
268 if (sidebar) {
269 sidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
270 }
271 }
272 }
273
274 function yatraScrollToContact() {
275 const contactSection = document.querySelector('#contact') || document.querySelector('.yatra-contact-form');
276 if (contactSection) {
277 contactSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
278 }
279 }
280
281
282 function yatraCloseMobileSidebar() {
283 console.log('Close button clicked');
284 const sidebar = document.getElementById('yatra-mobile-sticky-sidebar');
285 if (sidebar) {
286 console.log('Sidebar found, closing...');
287 sidebar.classList.add('user-closed');
288
289 // Save closed state in sessionStorage (persists until page reload)
290 sessionStorage.setItem('yatra-mobile-sidebar-closed', 'true');
291
292 // Hide sidebar with animation
293 setTimeout(() => {
294 sidebar.style.transform = 'translateY(100%)';
295 sidebar.style.opacity = '0';
296 }, 100);
297 } else {
298 console.log('Sidebar not found');
299 }
300 }
301
302 // Auto-show on page load (reset closed state)
303 document.addEventListener('DOMContentLoaded', function() {
304 console.log('DOM loaded, checking mobile sidebar...');
305 const sidebar = document.getElementById('yatra-mobile-sticky-sidebar');
306
307 if (sidebar) {
308 console.log('Mobile sidebar element found');
309
310 // Clear any previous closed state on page load
311 sessionStorage.removeItem('yatra-mobile-sidebar-closed');
312
313 // Ensure sidebar is visible on page load (CSS will handle responsive)
314 sidebar.classList.remove('user-closed');
315 sidebar.classList.remove('hidden');
316
317 console.log('CSS-based responsive behavior initialized');
318 } else {
319 console.log('Mobile sidebar element NOT found');
320 }
321
322 // Initialize mobile booking form functionality
323 initializeMobileBookingForm();
324 });
325
326 function initializeMobileBookingForm() {
327 // Mobile quantity controls
328 const mobileMinusBtns = document.querySelectorAll('.yatra-mobile-quantity-minus');
329 const mobilePlusBtns = document.querySelectorAll('.yatra-mobile-quantity-plus');
330
331 mobileMinusBtns.forEach(btn => {
332 btn.addEventListener('click', function() {
333 const targetId = this.getAttribute('data-target');
334 const input = document.getElementById(targetId);
335 if (input) {
336 const current = parseInt(input.value) || 1;
337 const min = parseInt(input.getAttribute('min')) || 1;
338 if (current > min) {
339 input.value = current - 1;
340 updateMobileTotal();
341 }
342 }
343 });
344 });
345
346 mobilePlusBtns.forEach(btn => {
347 btn.addEventListener('click', function() {
348 const targetId = this.getAttribute('data-target');
349 const input = document.getElementById(targetId);
350 if (input) {
351 const current = parseInt(input.value) || 1;
352 const max = parseInt(input.getAttribute('max')) || 20;
353 if (current < max) {
354 input.value = current + 1;
355 updateMobileTotal();
356 }
357 }
358 });
359 });
360
361 // Mobile date picker - Initialize exactly like main date picker
362 const mobileDateInput = document.getElementById('mobile_travel_date');
363 if (mobileDateInput) {
364 console.log('Initializing mobile date picker...');
365
366 // Check if Flatpickr is available
367 if (typeof flatpickr === 'undefined') {
368 console.log('Flatpickr not loaded, falling back to basic date input');
369 // Make it a regular date input as fallback
370 mobileDateInput.type = 'date';
371 mobileDateInput.removeAttribute('readonly');
372 } else {
373 // Get availability dates from global trip data with fallback
374 let availabilityDates = [];
375 try {
376 if (window.yatraTripData && Array.isArray(window.yatraTripData.availabilityDates)) {
377 availabilityDates = window.yatraTripData.availabilityDates;
378 }
379 } catch (e) {
380 console.log('Error accessing yatraTripData:', e);
381 }
382
383 var mobileFpCommon = {
384 clickOpens: true,
385 appendTo: document.body,
386 static: false,
387 onReady: function(selectedDates, dateStr, instance) {
388 if (instance.calendarContainer) {
389 instance.calendarContainer.style.zIndex = '100050';
390 }
391 },
392 onChange: function(selectedDates, dateStr, instance) {
393 var mainDateInput = document.getElementById('travel_date');
394 if (mainDateInput && mainDateInput._flatpickr) {
395 mainDateInput._flatpickr.setDate(dateStr);
396 }
397 updateMobileTotal();
398 }
399 };
400 function yatraBindMobileDatepickerTouch(inputEl) {
401 if (!inputEl || !inputEl._flatpickr) {
402 return;
403 }
404 var fp = inputEl._flatpickr;
405 var wrap = inputEl.closest('.yatra-mobile-compact-datepicker');
406 if (!wrap) {
407 return;
408 }
409 wrap.addEventListener('click', function(e) {
410 e.stopPropagation();
411 if (!fp.isOpen) {
412 fp.open();
413 }
414 });
415 }
416 // Initialize Flatpickr with same configuration as main date picker
417 if (availabilityDates.length > 0) {
418 // Availability-based booking - enable only specific dates
419 flatpickr(mobileDateInput, Object.assign({}, mobileFpCommon, {
420 dateFormat: 'Y-m-d',
421 minDate: 'today',
422 enable: availabilityDates
423 }));
424 } else {
425 // Regular booking - flexible dates with min/max constraints
426 var config = Object.assign({}, mobileFpCommon, {
427 dateFormat: 'Y-m-d',
428 minDate: 'today'
429 });
430 var minDate = mobileDateInput.getAttribute('data-min-date');
431 var maxDate = mobileDateInput.getAttribute('data-max-date');
432 if (minDate) {
433 config.minDate = minDate;
434 }
435 if (maxDate) {
436 config.maxDate = maxDate;
437 }
438 flatpickr(mobileDateInput, config);
439 }
440 yatraBindMobileDatepickerTouch(mobileDateInput);
441 }
442
443 console.log('Mobile date picker initialized successfully');
444 } else {
445 console.log('Mobile date input not found');
446 }
447
448 // Mobile check availability button
449 const mobileCheckBtn = document.getElementById('mobile-check-availability-btn');
450 if (mobileCheckBtn) {
451 mobileCheckBtn.addEventListener('click', function() {
452 console.log('Mobile Check button clicked');
453 // Sync mobile form data with main form
454 syncMobileToMainForm();
455 // Trigger main check availability
456 const mainCheckBtn = document.getElementById('check-availability-btn');
457 if (mainCheckBtn) {
458 console.log('Triggering main Check button');
459 mainCheckBtn.click();
460 } else {
461 console.log('Main Check button not found');
462 }
463 });
464 } else {
465 console.log('Mobile Check button not found');
466 }
467 }
468
469 function syncMobileToMainForm() {
470 // Sync date
471 const mobileDate = document.getElementById('mobile_travel_date');
472 const mainDate = document.getElementById('travel_date');
473 if (mobileDate && mainDate) {
474 // If main date has Flatpickr instance, use it
475 if (mainDate._flatpickr) {
476 mainDate._flatpickr.setDate(mobileDate.value);
477 } else {
478 mainDate.value = mobileDate.value;
479 }
480 }
481
482 // Sync travelers
483 const mobileTravelers = document.getElementById('mobile_num_travelers');
484 const mainTravelers = document.getElementById('num_travelers');
485 if (mobileTravelers && mainTravelers) {
486 mainTravelers.value = mobileTravelers.value;
487 }
488 }
489
490 function updateMobileTotal() {
491 const sidebar = document.getElementById('yatra-mobile-sticky-sidebar');
492 if (!sidebar) return;
493
494 const pricingType = sidebar.getAttribute('data-pricing-type');
495 const mobileTotal = document.getElementById('mobile-total-amount');
496 if (!mobileTotal) return;
497
498 let total = 0;
499
500 if (pricingType === 'traveler_based') {
501 // Traveler-based pricing: sum all category quantities × prices
502 const travelerInputs = document.querySelectorAll('[id^="mobile_traveler_"]');
503 travelerInputs.forEach(input => {
504 const qty = parseInt(input.value) || 0;
505 const price = parseFloat(input.getAttribute('data-price')) || 0;
506 total += qty * price;
507 });
508 } else {
509 // Regular pricing: simple multiplication
510 const travelersInput = document.getElementById('mobile_num_travelers');
511 if (travelersInput) {
512 const travelers = parseInt(travelersInput.value) || 1;
513 const basePrice = parseFloat(travelersInput.getAttribute('data-price')) || 0;
514 total = travelers * basePrice;
515 }
516 }
517
518 // Format and update display (mobile total element has been removed)
519 // The mobile total price display is no longer shown
520 if (mobileTotal) {
521 if (window.yatraTripData && window.yatraTripData.currencySymbol) {
522 mobileTotal.textContent = window.yatraTripData.currencySymbol + total.toFixed(2);
523 } else {
524 mobileTotal.textContent = formatCurrency(total);
525 }
526 }
527
528 // Also sync with main form total if it exists (main total also removed)
529 const mainTotal = document.getElementById('total-amount');
530 if (mainTotal && window.yatraTripData && window.yatraTripData.currencySymbol) {
531 mainTotal.textContent = window.yatraTripData.currencySymbol + total.toFixed(2);
532 }
533 }
534
535 function formatCurrency(amount) {
536 // Simple currency formatting (can be enhanced)
537 return '$' + amount.toFixed(2);
538 }
539
540 // Auto-hide on scroll down, show on scroll up
541 let lastScrollTop = 0;
542 const mobileSidebar = document.getElementById('yatra-mobile-sticky-sidebar');
543 const scrollThreshold = 100;
544
545 window.addEventListener('scroll', function() {
546 if (!mobileSidebar) return;
547
548 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
549
550 if (scrollTop > lastScrollTop && scrollTop > scrollThreshold) {
551 // Scrolling down - hide sidebar
552 mobileSidebar.classList.add('hidden');
553 } else {
554 // Scrolling up - show sidebar
555 mobileSidebar.classList.remove('hidden');
556 }
557
558 lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
559 }, false);
560 </script>
561