PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
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.14.2, at templates/partials/availability-card.php

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