PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / availability-card.php

availability-card.php in Yatra – Travel Booking & Tour Operator Software 3.0.3, at templates/partials/availability-card.php

559 lines 34.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Single departure card for trip availability (included in a loop).
4 * @package Yatra
5 * @var array $card @var int $index @var object $trip_data @var string $pricing_type @var string $selected_date_filter
6 */
7 defined('ABSPATH') || exit;
8 $card = $card ?? [];
9 $index = (int) ($index ?? 0);
10 $trip_data = $trip_data ?? (object) [];
11 $pricing_type = $pricing_type ?? 'regular';
12 $selected_date_filter = $selected_date_filter ?? '';
13 $trip_id = !empty($trip_data->id) ? (int) $trip_data->id : 0;
14 $max_travelers = (int) ($trip_data->max_travelers ?? 20);
15 $item_id = isset($card['id']) ? (string) $card['id'] : (string) $index;
16
17 // Get pricing from card (already calculated with dynamic pricing in Controller)
18 $sale_price = (float) ($card['sale_price'] ?? 0);
19 $original_price = (float) ($card['original_price'] ?? 0);
20
21 $initial_total_price = $sale_price;
22
23 $seats_available = (int) ($card['seats_available'] ?? 0);
24 $is_limited = !empty($card['is_limited']);
25 $card_status = $card['status'] ?? 'available';
26 $is_sold_out = isset($card['is_sold_out']) && $card['is_sold_out'];
27 if ($is_sold_out || $card_status === 'sold_out') {
28 $card_status = 'sold_out';
29 }
30
31 // Match Y-m-d even if DB returns datetime (e.g. 2026-08-13 00:00:00)
32 $yatra_card_date_norm = static function ($v): string {
33 $v = trim((string) $v);
34 return preg_match('/^(\d{4}-\d{2}-\d{2})/', $v, $m) ? $m[1] : $v;
35 };
36 $sel_norm = $yatra_card_date_norm($selected_date_filter ?? '');
37 $card_date_norm = $yatra_card_date_norm($card['data_date'] ?? '');
38 $is_selected_card = ($sel_norm !== '' && $card_date_norm !== '' && $sel_norm === $card_date_norm);
39 $should_be_open = $is_selected_card || $index === 0;
40
41 // Apply Yatra date format to availability header dates when the controller returns ISO.
42 $yatra_format_card_date_for_display = static function ($value): string {
43 $raw = trim((string) $value);
44 if ($raw === '') {
45 return '';
46 }
47 // Only transform ISO-like values; leave already-human strings unchanged.
48 if (!preg_match('/^\d{4}-\d{2}-\d{2}/', $raw)) {
49 return $raw;
50 }
51 return \Yatra\Helpers\FormatHelper::formatDate($raw);
52 };
53
54 $from_date_display = $yatra_format_card_date_for_display($card['from_date'] ?? '');
55 $to_date_display = $yatra_format_card_date_for_display($card['to_date'] ?? '');
56 $date_display_display = $yatra_format_card_date_for_display($card['date_display'] ?? '');
57
58 $dp_disp = $card['dynamic_pricing_display'] ?? [];
59 $dp_show_original = filter_var($dp_disp['show_original_price'] ?? true, FILTER_VALIDATE_BOOLEAN);
60 $dp_show_savings = filter_var($dp_disp['show_savings_badge'] ?? true, FILTER_VALIDATE_BOOLEAN);
61 $dp_show_urgency = filter_var($dp_disp['show_urgency_messages'] ?? false, FILTER_VALIDATE_BOOLEAN);
62 $dp_urgency_messages = [];
63 if (!empty($card['dynamic_pricing_urgency_messages']) && is_array($card['dynamic_pricing_urgency_messages'])) {
64 $dp_urgency_messages = $card['dynamic_pricing_urgency_messages'];
65 }
66
67 $dp_enabled = (bool) apply_filters('yatra_dynamic_pricing_enabled', false);
68
69 // Display amounts for this card (must match traveler row data-price, including per-category DP).
70 $display_sale_price = $sale_price;
71 $display_original_price = $original_price;
72 $card_pricing_type_header = $card['pricing_type'] ?? $pricing_type ?? 'regular';
73 if ($card_pricing_type_header === 'traveler_based' && !empty($card['traveler_pricing']) && is_array($card['traveler_pricing'])) {
74 $first_traveler = $card['traveler_pricing'][0];
75 $first_traveler = is_array($first_traveler) ? (object) $first_traveler : $first_traveler;
76 $pt_price = 0.0;
77 if (isset($first_traveler->effective_price) && (float) $first_traveler->effective_price > 0) {
78 $pt_price = (float) $first_traveler->effective_price;
79 } elseif (isset($first_traveler->discounted_price) && (float) $first_traveler->discounted_price > 0) {
80 $pt_price = (float) $first_traveler->discounted_price;
81 } elseif (isset($first_traveler->original_price) && (float) $first_traveler->original_price > 0) {
82 $pt_price = (float) $first_traveler->original_price;
83 }
84 if ($dp_enabled && $pt_price > 0) {
85 $pt_orig_dp = (float) ($first_traveler->original_price ?? 0);
86 $pt_disc_dp = (float) ($first_traveler->discounted_price ?? $first_traveler->sale_price ?? $first_traveler->effective_price ?? 0);
87 if ($pt_disc_dp <= 0) {
88 $pt_disc_dp = $pt_price;
89 }
90 $pt_price = (float) apply_filters('yatra_availability_price', $pt_price, $trip_id, [
91 'departure_date' => $card['date'] ?? null,
92 'spots_remaining' => $card['spots_remaining'] ?? null,
93 'availability_id' => $item_id,
94 'price_type_id' => $first_traveler->id ?? ($first_traveler->price_type_id ?? null),
95 'original_price' => $pt_orig_dp > 0 ? $pt_orig_dp : $pt_price,
96 'discounted_price' => $pt_disc_dp > 0 ? $pt_disc_dp : $pt_price,
97 ]);
98 }
99 if ($pt_price > 0) {
100 $display_sale_price = $pt_price;
101 }
102 $fo = (float) ($first_traveler->original_price ?? 0);
103 if ($fo > 0) {
104 $display_original_price = $fo;
105 }
106 }
107 $yatra_av_badges_row_original = $dp_show_original && $display_original_price > $display_sale_price && $display_original_price > 0;
108 $yatra_av_badges_row_savings = $dp_show_savings && !empty($card['discount_text']);
109 $yatra_av_discount_is_surge = $yatra_av_badges_row_savings && strpos((string) $card['discount_text'], '+') === 0;
110 $initial_total_price = $display_sale_price;
111 ?>
112 <div class="yatra-availability-card <?php echo $should_be_open ? 'open' : ''; ?> <?php echo $is_selected_card ? 'selected' : ''; ?> <?php echo $is_limited ? 'limited' : ''; ?> yatra-card-status-<?php echo esc_attr($card_status); ?>"
113 data-availability-id="<?php echo esc_attr($item_id); ?>"
114 data-month="<?php echo esc_attr($card['data_month'] ?? ''); ?>"
115 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
116 data-price="<?php echo esc_attr($display_sale_price); ?>"
117 data-seats="<?php echo esc_attr($seats_available); ?>"
118 data-item="<?php echo esc_attr($item_id); ?>"
119 data-index="<?php echo esc_attr($index); ?>">
120
121 <!-- Card Header (Clickable) -->
122 <div class="yatra-availability-card-header yatra-availability-toggle" role="button" tabindex="0" aria-label="<?php esc_attr_e('Toggle availability details', 'yatra'); ?>">
123
124 <?php if (!empty($card['date_display'])): ?>
125 <!-- Day Trip: Show date as header -->
126 <div class="yatra-card-date-header">
127 <?php echo yatra_svg_icon('calendar', 'yatra-icon-sm'); ?>
128 <?php echo esc_html($date_display_display); ?>
129 </div>
130 <?php endif; ?>
131
132 <!-- Badge row: own line below day-trip date (when present), above the main grid — avoids overlap with price/seats -->
133 <div class="yatra-card-badges-container">
134 <?php if ($yatra_av_badges_row_original) : ?>
135 <span class="yatra-availability-badge-row__original" title="<?php esc_attr_e('Original price', 'yatra'); ?>"><?php echo yatra_format_price($display_original_price); ?></span>
136 <?php endif; ?>
137 <?php if ($yatra_av_badges_row_savings) : ?>
138 <span class="yatra-card-discount-badge <?php echo $yatra_av_discount_is_surge ? 'yatra-badge-higher' : 'yatra-badge-discount'; ?>"><?php echo esc_html((string) $card['discount_text']); ?></span>
139 <?php endif; ?>
140 <?php if ($dp_show_urgency && !empty($dp_urgency_messages)) : ?>
141 <?php foreach ($dp_urgency_messages as $dp_urgency_line) : ?>
142 <span class="yatra-availability-urgency-message"><?php echo esc_html((string) $dp_urgency_line); ?></span>
143 <?php endforeach; ?>
144 <?php endif; ?>
145 <?php
146 $card_status = $card['status'] ?? 'available';
147 $is_sold_out = isset($card['is_sold_out']) && $card['is_sold_out'];
148
149 if ($is_sold_out || $card_status === 'sold_out') {
150 echo '<div class="yatra-badge-status yatra-badge-sold-out">' . esc_html__('Sold Out', 'yatra') . '</div>';
151 } elseif ($card_status === 'limited') {
152 echo '<div class="yatra-badge-status yatra-badge-limited">' . esc_html__('Limited', 'yatra') . '</div>';
153 } elseif ($card_status === 'blocked') {
154 echo '<div class="yatra-badge-status yatra-badge-blocked">' . esc_html__('Blocked', 'yatra') . '</div>';
155 } elseif ($card_status === 'closed') {
156 echo '<div class="yatra-badge-status yatra-badge-closed">' . esc_html__('Closed', 'yatra') . '</div>';
157 } elseif ($card_status === 'cancelled') {
158 echo '<div class="yatra-badge-status yatra-badge-cancelled">' . esc_html__('Cancelled', 'yatra') . '</div>';
159 } else {
160 echo '<div class="yatra-badge-status yatra-badge-available">' . esc_html__('Available', 'yatra') . '</div>';
161 }
162 ?>
163 </div>
164
165 <div class="yatra-card-header-grid <?php echo !empty($card['date_display']) ? 'yatra-day-trip-grid' : ''; ?>">
166
167 <!-- Start/Departure -->
168 <div class="yatra-card-header-item">
169 <div class="yatra-card-header-label"><?php echo esc_html($card['from_label'] ?? __('Departure', 'yatra')); ?></div>
170 <div class="yatra-card-header-date"><?php echo esc_html($from_date_display); ?></div>
171 <div class="yatra-card-header-location"><?php echo esc_html($card['from_location'] ?? ''); ?></div>
172 </div>
173
174 <!-- End/Return -->
175 <div class="yatra-card-header-item">
176 <div class="yatra-card-header-label"><?php echo esc_html($card['to_label'] ?? __('Return', 'yatra')); ?></div>
177 <div class="yatra-card-header-date"><?php echo esc_html($to_date_display); ?></div>
178 <div class="yatra-card-header-location"><?php echo esc_html($card['to_location'] ?? ''); ?></div>
179 </div>
180
181 <!-- Seats -->
182 <div class="yatra-card-header-item">
183 <div class="yatra-card-header-label"><?php esc_html_e('Seats remaining', 'yatra'); ?></div>
184 <div class="yatra-card-header-seats <?php echo $is_limited ? 'limited' : ''; ?>"><?php echo esc_html($card['seats'] ?? '0'); ?></div>
185 <div class="yatra-card-header-sub"><?php esc_html_e('per departure', 'yatra'); ?></div>
186 </div>
187
188 <!-- Price (sale only; struck original + % badge sit in .yatra-card-badges-container with status) -->
189 <div class="yatra-card-header-price">
190 <div class="yatra-card-price-group yatra-card-price-group--stack">
191 <?php
192 echo '<span class="yatra-sale-price">' . yatra_format_price($display_sale_price) . '</span>';
193 ?>
194 </div>
195 <span class="yatra-price-label"><?php echo esc_html__('per person', 'yatra'); ?></span>
196 </div>
197
198 <!-- Toggle Arrow -->
199 <div class="yatra-card-header-arrow">
200 <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
201 <path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
202 </svg>
203 </div>
204 </div>
205 </div>
206
207 <!-- Card Body (Expandable) -->
208 <div class="yatra-availability-card-body">
209 <div class="yatra-card-body-content">
210 <div class="yatra-card-info-grid">
211 <div class="yatra-card-info-item">
212 <div class="yatra-card-info-icon">
213 <?php echo yatra_svg_icon('calendar', 'yatra-icon-sm'); ?>
214 </div>
215 <div class="yatra-card-info-content">
216 <div class="yatra-card-info-label"><?php esc_html_e('Duration', 'yatra'); ?></div>
217 <div class="yatra-card-info-value">
218 <?php
219 $duration_days = (int) ($trip_data->duration_days ?? 1);
220 echo esc_html($duration_days . ' ' . _n('Day', 'Days', $duration_days, 'yatra'));
221 ?>
222 </div>
223 </div>
224 </div>
225
226 <div class="yatra-card-info-item">
227 <div class="yatra-card-info-icon">
228 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
229 </div>
230 <div class="yatra-card-info-content">
231 <div class="yatra-card-info-label"><?php esc_html_e('Seats left', 'yatra'); ?></div>
232 <div class="yatra-card-info-value"><?php echo esc_html(sprintf(_n('%d seat', '%d seats', $seats_available, 'yatra'), $seats_available)); ?></div>
233 </div>
234 </div>
235
236 <?php
237 $cancellation_policy = '';
238 if (method_exists($trip_data, 'getCancellationPolicy')) {
239 $cancellation_policy = $trip_data->getCancellationPolicy();
240 } elseif (isset($trip_data->cancellation_policy)) {
241 $cancellation_policy = $trip_data->cancellation_policy;
242 }
243
244 $cancellation_label = '';
245 $cancellation_value = '';
246 if (!empty($cancellation_policy)) {
247 // Extract cancellation info from policy
248 $policy_lower = strtolower($cancellation_policy);
249 if (strpos($policy_lower, 'free') !== false || strpos($policy_lower, 'no charge') !== false) {
250 $cancellation_label = __('Free Cancellation', 'yatra');
251 if (preg_match('/(\d+)\s*(hour|day|week)s?\s*before/i', $cancellation_policy, $matches)) {
252 $time_value = $matches[1];
253 $time_unit = $matches[2];
254 $cancellation_value = sprintf(
255 __('Up to %d %s%s before', 'yatra'),
256 $time_value,
257 $time_unit,
258 $time_value > 1 ? 's' : ''
259 );
260 } else {
261 $cancellation_value = __('No charge', 'yatra');
262 }
263 } else {
264 // Non-free cancellation
265 $cancellation_label = __('Cancellation Policy', 'yatra');
266 $cancellation_value = __('See details', 'yatra');
267 }
268 } else {
269 // Default fallback when no policy is set
270 $cancellation_label = __('Cancellation Policy', 'yatra');
271 $cancellation_value = __('May apply', 'yatra');
272 }
273 ?>
274 <div class="yatra-card-info-item">
275 <div class="yatra-card-info-icon">
276 <?php echo yatra_svg_icon('check', 'yatra-icon-sm'); ?>
277 </div>
278 <div class="yatra-card-info-content">
279 <div class="yatra-card-info-label"><?php echo esc_html($cancellation_label); ?></div>
280 <div class="yatra-card-info-value"><?php echo esc_html($cancellation_value); ?></div>
281 </div>
282 </div>
283 </div>
284
285 <div class="yatra-card-traveler-section">
286 <label class="yatra-card-traveler-label"><?php esc_html_e('Travelers', 'yatra'); ?></label>
287
288 <?php
289 // Use card-level pricing type as source of truth
290 $card_pricing_type = $card['pricing_type'] ?? $pricing_type ?? 'regular';
291 $card_price_types = [];
292
293 // Use availability-specific traveler_pricing for traveler-based pricing
294 if ($card_pricing_type === 'traveler_based' && !empty($card['traveler_pricing'])) {
295 $card_price_types = $card['traveler_pricing'];
296 }
297
298 // Normalize traveler pricing to objects if array
299 $normalized_price_types = [];
300 foreach ($card_price_types as $cpt) {
301 if (is_array($cpt)) {
302 $normalized_price_types[] = (object) $cpt;
303 } else {
304 $normalized_price_types[] = $cpt;
305 }
306 }
307 ?>
308
309 <?php if ($card_pricing_type === 'traveler_based' && !empty($normalized_price_types)): ?>
310 <!-- Traveler-based pricing: Show dynamic categories -->
311 <?php
312 // Build traveler rows directly from card-level traveler_pricing data
313 $traveler_rows = [];
314 $display_parts = [];
315 foreach ($normalized_price_types as $pt_index => $pt) {
316 $pt_label = $pt->category_label ?? $pt->label ?? __('Traveler', 'yatra');
317 $pt_pricing_mode = $pt->pricing_mode ?? 'per_person';
318 $pt_is_per_group = ($pt_pricing_mode === 'per_group');
319 $pt_category_id = $pt->category_id ?? $pt_index;
320 $pt_default = $pt_index === 0 ? 1 : 0;
321 $pt_min_qty = 0;
322 $pt_max_qty = (int) min($seats_available, $max_travelers);
323
324 // Determine price with fallback chain
325 $pt_price = 0;
326 if (isset($pt->effective_price) && $pt->effective_price > 0) {
327 $pt_price = (float) $pt->effective_price;
328 } elseif (isset($pt->discounted_price) && $pt->discounted_price > 0) {
329 $pt_price = (float) $pt->discounted_price;
330 } elseif (isset($pt->original_price) && $pt->original_price > 0) {
331 $pt_price = (float) $pt->original_price;
332 }
333
334 // Apply dynamic pricing to traveler category prices
335 if ($dp_enabled && $pt_price > 0) {
336 $pt_orig_dp = (float) ($pt->original_price ?? 0);
337 $pt_disc_dp = (float) ($pt->discounted_price ?? $pt->sale_price ?? $pt->effective_price ?? 0);
338 if ($pt_disc_dp <= 0) {
339 $pt_disc_dp = $pt_price;
340 }
341 $pt_price = apply_filters('yatra_availability_price', $pt_price, $trip_id, [
342 'departure_date' => $card['date'] ?? null,
343 'spots_remaining' => $card['spots_remaining'] ?? null,
344 'availability_id' => $item_id,
345 'price_type_id' => $pt->id ?? ($pt->price_type_id ?? null),
346 'original_price' => $pt_orig_dp > 0 ? $pt_orig_dp : $pt_price,
347 'discounted_price' => $pt_disc_dp > 0 ? $pt_disc_dp : $pt_price,
348 ]);
349 }
350
351 // Age info
352 $pt_age_text = '';
353 $pt_age_min = isset($pt->age_min) ? (int) $pt->age_min : null;
354 $pt_age_max = isset($pt->age_max) ? (int) $pt->age_max : null;
355 if ($pt_age_min !== null || $pt_age_max !== null) {
356 if ($pt_age_min !== null && $pt_age_max !== null) {
357 $pt_age_text = sprintf(__('(Age %d-%d)', 'yatra'), $pt_age_min, $pt_age_max);
358 } elseif ($pt_age_min !== null) {
359 $pt_age_text = sprintf(__('(Age %d+)', 'yatra'), $pt_age_min);
360 } else {
361 $pt_age_text = sprintf(__('(Up to age %d)', 'yatra'), $pt_age_max);
362 }
363 }
364
365 // Build pricing label for per_group mode
366 $pricing_label = '';
367 if ($pt_is_per_group) {
368 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
369 $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
370 } elseif (!empty($pt->max_pax)) {
371 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
372 } elseif (!empty($pt->min_pax)) {
373 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
374 } else {
375 $pricing_label = __('per group', 'yatra');
376 }
377 }
378
379 // Build price HTML
380 $pt_price_html = '';
381 if ($pt_price > 0) {
382 $pt_price_html = '<div class="yatra-quantity-price-wrapper">';
383 $pt_price_html .= '<span class="yatra-quantity-price">' . yatra_format_price($pt_price) . '</span>';
384 if ($pt_is_per_group) {
385 $pt_price_html .= '<span class="yatra-pricing-mode-label yatra-pricing-mode-group">' . esc_html($pricing_label) . '</span>';
386 }
387 $pt_price_html .= '</div>';
388 }
389
390 $input_id = 'traveler_' . $pt_category_id;
391
392 $traveler_rows[] = [
393 'label' => $pt_label,
394 'subtitle' => $pt_age_text,
395 'price_html' => $pt_price_html,
396 'row_attrs' => [
397 'data-category-id' => $pt_category_id,
398 'data-price' => $pt_price,
399 'data-pricing-mode' => $pt_pricing_mode,
400 ],
401 'minus_disabled' => $pt_default <= $pt_min_qty,
402 'plus_disabled' => $pt_default >= $pt_max_qty,
403 'minus_attrs' => [
404 'data-target' => $input_id,
405 'data-item' => $item_id,
406 'data-category' => $pt_category_id,
407 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
408 ],
409 'plus_attrs' => [
410 'data-target' => $input_id,
411 'data-item' => $item_id,
412 'data-category' => $pt_category_id,
413 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
414 ],
415 'input_attrs' => [
416 'id' => $input_id,
417 'name' => 'travelers[' . $pt_category_id . ']',
418 'value' => $pt_default,
419 'min' => $pt_min_qty,
420 'max' => $pt_max_qty,
421 'data-item' => $item_id,
422 'data-category' => $pt_category_id,
423 'data-category-label' => $pt_label,
424 'data-price' => $pt_price,
425 'data-pricing-mode' => $pt_pricing_mode,
426 ],
427 ];
428
429 if ($pt_default > 0) {
430 $display_parts[] = $pt_label . ' x ' . $pt_default;
431 }
432 }
433
434 $traveler_display_text = !empty($display_parts) ? implode(', ', $display_parts) : __('Select travelers', 'yatra');
435
436 // Setup variables for traveler-selector.php using availability-specific data
437 $root_id = '';
438 $root_class = 'yatra-booking-field-select yatra-participants-select yatra-availability-participants';
439 $container_attrs = [
440 'data-item' => $item_id,
441 ];
442
443 $display_id = 'participants-display';
444 $display_class = 'yatra-participants-display yatra-availability-participants-display';
445 $display_attrs = [
446 'data-item' => $item_id,
447 ];
448
449 $dropdown_id = '';
450 $dropdown_class = 'yatra-booking-quantity-selector yatra-availability-quantity-selector';
451 $dropdown_attrs = [
452 'data-item' => $item_id,
453 ];
454
455 $display_text = $traveler_display_text;
456 $icon_html = yatra_svg_icon('users', 'yatra-icon-sm');
457 $rows = $traveler_rows;
458
459 include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php';
460 ?>
461 <?php else: ?>
462 <!-- Regular pricing: Show simple number of travelers -->
463 <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>">
464 <div class="yatra-booking-field-icon">
465 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
466 </div>
467 <div class="yatra-quantity-controls-inline">
468 <button type="button" class="yatra-quantity-btn yatra-quantity-minus" data-target="num-travelers-<?php echo esc_attr($item_id); ?>" aria-label="<?php esc_attr_e('Decrease travelers', 'yatra'); ?>" disabled>
469 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
470 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
471 </svg>
472 </button>
473 <input type="number"
474 id="num-travelers-<?php echo esc_attr($item_id); ?>"
475 class="yatra-quantity-input-simple yatra-availability-num-travelers"
476 data-item="<?php echo esc_attr($item_id); ?>"
477 value="1"
478 min="1"
479 max="<?php echo esc_attr(min($seats_available, $max_travelers)); ?>"
480 readonly
481 data-price="<?php echo esc_attr($display_sale_price); ?>">
482 <button type="button" class="yatra-quantity-btn yatra-quantity-plus" data-target="num-travelers-<?php echo esc_attr($item_id); ?>" aria-label="<?php esc_attr_e('Increase travelers', 'yatra'); ?>">
483 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
484 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
485 </svg>
486 </button>
487 </div>
488 <span class="yatra-travelers-label"><?php esc_html_e('travelers', 'yatra'); ?></span>
489 </div>
490 <?php endif; ?>
491
492 <div class="yatra-card-action-help"><?php esc_html_e('Select travelers to update the total, then click Book Now.', 'yatra'); ?></div>
493 </div>
494 </div>
495
496 <!-- Booking Row at Bottom -->
497 <div class="yatra-card-booking-row">
498 <div class="yatra-card-total-box">
499 <div class="yatra-card-total-label"><?php esc_html_e('Total', 'yatra'); ?></div>
500 <div class="yatra-card-total-note" data-item="<?php echo esc_attr($item_id); ?>"><?php esc_html_e('for 1 traveler', 'yatra'); ?></div>
501 <div class="yatra-card-total-amount" data-item="<?php echo esc_attr($item_id); ?>" data-base-price="<?php echo esc_attr($initial_total_price); ?>">
502 <?php
503 $formatted_total = yatra_format_price($initial_total_price);
504 echo esc_html($formatted_total);
505 ?>
506 </div>
507 </div>
508 <?php
509 // Prepare price_types JSON for JavaScript
510 $price_types_json = '';
511 if (!empty($normalized_price_types)) {
512 $price_types_for_js = array_map(function($pt) {
513 $pt = (object) $pt;
514 return [
515 'category_id' => $pt->category_id ?? null,
516 'category_label' => $pt->category_label ?? $pt->label ?? '',
517 'original_price' => $pt->original_price ?? 0,
518 'sale_price' => $pt->sale_price ?? null,
519 'discounted_price' => $pt->discounted_price ?? null,
520 'effective_price' => $pt->effective_price ?? $pt->sale_price ?? $pt->discounted_price ?? $pt->original_price ?? 0,
521 'age_min' => $pt->age_min ?? null,
522 'age_max' => $pt->age_max ?? null,
523 ];
524 }, $normalized_price_types);
525 $price_types_json = wp_json_encode($price_types_for_js);
526 }
527
528 // Get departure time - prefer card data, fallback to from_date if it looks like time
529 $card_departure_time = '';
530 if (!empty($card['departure_time'])) {
531 $card_departure_time = $card['departure_time'];
532 } elseif (!empty($card['from_date']) && preg_match('/^\d{1,2}:\d{2}\s*(AM|PM)?$/i', $card['from_date'])) {
533 $card_departure_time = $card['from_date'];
534 }
535
536 // Is this a day trip?
537 $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']);
538 ?>
539 <button type="button"
540 class="yatra-card-book-btn"
541 data-trip-id="<?php echo esc_attr($trip_id); ?>"
542 data-availability-id="<?php echo esc_attr($item_id); ?>"
543 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
544 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
545 data-is-day-trip="<?php echo $is_card_day_trip ? '1' : '0'; ?>"
546 data-pricing-type="<?php echo esc_attr($card_pricing_type); ?>"
547 data-price-types="<?php echo esc_attr($price_types_json); ?>"
548 data-price="<?php echo esc_attr($display_sale_price); ?>"
549 data-item="<?php echo esc_attr($item_id); ?>">
550 <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?>
551 <span class="yatra-card-book-btn-text">
552 <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span>
553 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span>
554 </span>
555 </button>
556 </div>
557 </div>
558 </div>
559