PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / templates / partials / availability-card.php

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

653 lines 40.7 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 $duration_hours = (int) ($trip_data->duration_hours ?? 0);
244 if ($duration_hours > 0) {
245 // Hour-based day tour: "8 Hours", not "1 Day".
246 echo esc_html(sprintf(
247 /* translators: %d: number of hours. */
248 _n('%d Hour', '%d Hours', $duration_hours, 'yatra'),
249 $duration_hours
250 ));
251 } else {
252 echo esc_html($duration_days . ' ' . _n('Day', 'Days', $duration_days, 'yatra'));
253 }
254 ?>
255 </div>
256 </div>
257 </div>
258
259 <div class="yatra-card-info-item">
260 <div class="yatra-card-info-icon">
261 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
262 </div>
263 <div class="yatra-card-info-content">
264 <div class="yatra-card-info-label"><?php esc_html_e('Seats left', 'yatra'); ?></div>
265 <div class="yatra-card-info-value"><?php
266 /* translators: %d: number of seats remaining. */
267 echo esc_html(sprintf(_n('%d seat', '%d seats', $seats_available, 'yatra'), $seats_available));
268 ?></div>
269 </div>
270 </div>
271
272 <?php
273 $cancellation_policy = '';
274 if (method_exists($trip_data, 'getCancellationPolicy')) {
275 $cancellation_policy = $trip_data->getCancellationPolicy();
276 } elseif (isset($trip_data->cancellation_policy)) {
277 $cancellation_policy = $trip_data->cancellation_policy;
278 }
279
280 $cancellation_label = '';
281 $cancellation_value = '';
282 if (!empty($cancellation_policy)) {
283 // Extract cancellation info from policy
284 $policy_lower = strtolower($cancellation_policy);
285 if (strpos($policy_lower, 'free') !== false || strpos($policy_lower, 'no charge') !== false) {
286 $cancellation_label = __('Free Cancellation', 'yatra');
287 if (preg_match('/(\d+)\s*(hour|day|week)s?\s*before/i', $cancellation_policy, $matches)) {
288 $time_value = $matches[1];
289 $time_unit = $matches[2];
290 $cancellation_value = sprintf(
291 /* translators: 1: numeric amount, 2: time unit (hour/day/week), 3: plural "s" suffix if applicable. */
292 __('Up to %1$d %2$s%3$s before', 'yatra'),
293 $time_value,
294 $time_unit,
295 $time_value > 1 ? 's' : ''
296 );
297 } else {
298 $cancellation_value = __('No charge', 'yatra');
299 }
300 } else {
301 // Non-free cancellation
302 $cancellation_label = __('Cancellation Policy', 'yatra');
303 $cancellation_value = __('See details', 'yatra');
304 }
305 } else {
306 // Default fallback when no policy is set
307 $cancellation_label = __('Cancellation Policy', 'yatra');
308 $cancellation_value = __('May apply', 'yatra');
309 }
310 ?>
311 <div class="yatra-card-info-item">
312 <div class="yatra-card-info-icon">
313 <?php echo yatra_svg_icon('check', 'yatra-icon-sm'); ?>
314 </div>
315 <div class="yatra-card-info-content">
316 <div class="yatra-card-info-label"><?php echo esc_html($cancellation_label); ?></div>
317 <div class="yatra-card-info-value"><?php echo esc_html($cancellation_value); ?></div>
318 </div>
319 </div>
320 </div>
321
322 <div class="yatra-card-traveler-section">
323 <label class="yatra-card-traveler-label"><?php esc_html_e('Travelers', 'yatra'); ?></label>
324
325 <?php
326 // Use card-level pricing type as source of truth
327 $card_pricing_type = $card['pricing_type'] ?? $pricing_type ?? 'regular';
328 $card_price_types = [];
329
330 // Use availability-specific traveler_pricing for traveler-based pricing
331 if ($card_pricing_type === 'traveler_based' && !empty($card['traveler_pricing'])) {
332 $card_price_types = $card['traveler_pricing'];
333 }
334
335 // Normalize traveler pricing to objects if array
336 $normalized_price_types = [];
337 foreach ($card_price_types as $cpt) {
338 if (is_array($cpt)) {
339 $normalized_price_types[] = (object) $cpt;
340 } else {
341 $normalized_price_types[] = $cpt;
342 }
343 }
344 ?>
345
346 <?php if ($card_pricing_type === 'traveler_based' && !empty($normalized_price_types)): ?>
347 <!-- Traveler-based pricing: Show dynamic categories -->
348 <?php
349 // Build traveler rows directly from card-level traveler_pricing data
350 $traveler_rows = [];
351 $display_parts = [];
352 foreach ($normalized_price_types as $pt_index => $pt) {
353 $pt_label = $pt->category_label ?? $pt->label ?? __('Traveler', 'yatra');
354 $pt_pricing_mode = $pt->pricing_mode ?? 'per_person';
355 $pt_is_per_group = ($pt_pricing_mode === 'per_group');
356 $pt_category_id = $pt->category_id ?? $pt_index;
357 $pt_default = $pt_index === 0 ? 1 : 0;
358 $pt_min_qty = 0;
359 $pt_max_qty = (int) min($seats_available, $max_travelers);
360 // Cap a per-group "block" category at its max group
361 // size (still bounded by seats). "per_block" mode may
362 // exceed it (additional group blocks), so skip.
363 if ($pt_is_per_group && !empty($pt->max_pax)
364 && (($pt->group_overflow ?? 'block') !== 'per_block')) {
365 $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax);
366 }
367 // A sold-out departure has no seats left to offer, so the
368 // first category must not still open at 1 traveler. Clamping
369 // here also disables both +/- buttons below (0 <= 0 >= 0).
370 if ($pt_default > $pt_max_qty) {
371 $pt_default = $pt_max_qty;
372 }
373
374 // Determine price with fallback chain
375 $pt_price = 0;
376 if (isset($pt->effective_price) && $pt->effective_price > 0) {
377 $pt_price = (float) $pt->effective_price;
378 } elseif (isset($pt->discounted_price) && $pt->discounted_price > 0) {
379 $pt_price = (float) $pt->discounted_price;
380 } elseif (isset($pt->original_price) && $pt->original_price > 0) {
381 $pt_price = (float) $pt->original_price;
382 }
383
384 // Apply dynamic pricing to traveler category prices
385 if ($dp_enabled && $pt_price > 0) {
386 $pt_orig_dp = (float) ($pt->original_price ?? 0);
387 $pt_disc_dp = (float) ($pt->discounted_price ?? $pt->sale_price ?? $pt->effective_price ?? 0);
388 if ($pt_disc_dp <= 0) {
389 $pt_disc_dp = $pt_price;
390 }
391 $pt_price = apply_filters('yatra_availability_price', $pt_price, $trip_id, [
392 'departure_date' => $card['date'] ?? null,
393 'spots_remaining' => $card['spots_remaining'] ?? null,
394 'availability_id' => $item_id,
395 'price_type_id' => $pt->id ?? ($pt->price_type_id ?? null),
396 'original_price' => $pt_orig_dp > 0 ? $pt_orig_dp : $pt_price,
397 'discounted_price' => $pt_disc_dp > 0 ? $pt_disc_dp : $pt_price,
398 ]);
399 }
400
401 // Age info
402 $pt_age_text = '';
403 $pt_age_min = isset($pt->age_min) ? (int) $pt->age_min : null;
404 $pt_age_max = isset($pt->age_max) ? (int) $pt->age_max : null;
405 if ($pt_age_min !== null || $pt_age_max !== null) {
406 if ($pt_age_min !== null && $pt_age_max !== null) {
407 /* translators: 1: minimum age, 2: maximum age. */
408 $pt_age_text = sprintf(__('(Age %1$d-%2$d)', 'yatra'), $pt_age_min, $pt_age_max);
409 } elseif ($pt_age_min !== null) {
410 /* translators: %d: minimum age. */
411 $pt_age_text = sprintf(__('(Age %d+)', 'yatra'), $pt_age_min);
412 } else {
413 /* translators: %d: maximum age. */
414 $pt_age_text = sprintf(__('(Up to age %d)', 'yatra'), $pt_age_max);
415 }
416 }
417
418 // Build pricing label for per_group mode
419 $pricing_label = '';
420 if ($pt_is_per_group) {
421 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
422 /* translators: 1: minimum pax for the group price, 2: maximum pax. */
423 $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $pt->min_pax, $pt->max_pax);
424 } elseif (!empty($pt->max_pax)) {
425 /* translators: %d: maximum pax for the group price. */
426 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
427 } elseif (!empty($pt->min_pax)) {
428 /* translators: %d: minimum pax for the group price. */
429 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
430 } else {
431 $pricing_label = __('per group', 'yatra');
432 }
433 }
434
435 // Build price HTML
436 $pt_price_html = '';
437 if ($pt_price > 0) {
438 $pt_price_html = '<div class="yatra-quantity-price-wrapper">';
439 $pt_price_html .= '<span class="yatra-quantity-price">' . yatra_format_price($pt_price) . '</span>';
440 if ($pt_is_per_group) {
441 $pt_price_html .= '<span class="yatra-pricing-mode-label yatra-pricing-mode-group">' . esc_html($pricing_label) . '</span>';
442 }
443 $pt_price_html .= '</div>';
444 }
445
446 $input_id = 'traveler_' . $pt_category_id;
447
448 $traveler_rows[] = [
449 'label' => $pt_label,
450 'subtitle' => $pt_age_text,
451 'price_html' => $pt_price_html,
452 'row_attrs' => [
453 'data-category-id' => $pt_category_id,
454 'data-price' => $pt_price,
455 'data-pricing-mode' => $pt_pricing_mode,
456 'data-group-overflow' => $pt->group_overflow ?? 'block',
457 'data-max-pax' => $pt->max_pax ?? '',
458 ],
459 'minus_disabled' => $pt_default <= $pt_min_qty,
460 'plus_disabled' => $pt_default >= $pt_max_qty,
461 'minus_attrs' => [
462 'data-target' => $input_id,
463 'data-item' => $item_id,
464 'data-category' => $pt_category_id,
465 /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
466 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
467 ],
468 'plus_attrs' => [
469 'data-target' => $input_id,
470 'data-item' => $item_id,
471 'data-category' => $pt_category_id,
472 /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
473 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
474 ],
475 'input_attrs' => [
476 'id' => $input_id,
477 'name' => 'travelers[' . $pt_category_id . ']',
478 'value' => $pt_default,
479 'min' => $pt_min_qty,
480 'max' => $pt_max_qty,
481 'data-item' => $item_id,
482 'data-category' => $pt_category_id,
483 'data-category-label' => $pt_label,
484 'data-price' => $pt_price,
485 'data-pricing-mode' => $pt_pricing_mode,
486 'data-group-overflow' => $pt->group_overflow ?? 'block',
487 'data-max-pax' => $pt->max_pax ?? '',
488 ],
489 ];
490
491 if ($pt_default > 0) {
492 $display_parts[] = $pt_label . ' x ' . $pt_default;
493 }
494 }
495
496 $traveler_display_text = !empty($display_parts) ? implode(', ', $display_parts) : __('Select travelers', 'yatra');
497
498 // Setup variables for traveler-selector.php using availability-specific data
499 $root_id = '';
500 $root_class = 'yatra-booking-field-select yatra-participants-select yatra-availability-participants';
501 $container_attrs = [
502 'data-item' => $item_id,
503 ];
504
505 $display_id = 'participants-display';
506 $display_class = 'yatra-participants-display yatra-availability-participants-display';
507 $display_attrs = [
508 'data-item' => $item_id,
509 ];
510
511 $dropdown_id = '';
512 $dropdown_class = 'yatra-booking-quantity-selector yatra-availability-quantity-selector';
513 $dropdown_attrs = [
514 'data-item' => $item_id,
515 ];
516
517 $display_text = $traveler_display_text;
518 $icon_html = yatra_svg_icon('users', 'yatra-icon-sm');
519 $rows = $traveler_rows;
520
521 include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php';
522 ?>
523 <?php else: ?>
524 <!-- Regular pricing: Show simple number of travelers -->
525 <?php
526 // A sold-out departure offers no seats, so the counter must start
527 // at 0 (not 1) and both controls stay disabled. Only the ceiling
528 // was capacity-aware before, which left sold-out cards showing a
529 // selectable traveler.
530 $simple_max_qty = (int) min($seats_available, $max_travelers);
531 $simple_qty = $simple_max_qty > 0 ? 1 : 0;
532 $simple_min_qty = $simple_qty;
533 ?>
534 <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>">
535 <div class="yatra-booking-field-icon">
536 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
537 </div>
538 <div class="yatra-quantity-controls-inline">
539 <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>
540 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
541 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
542 </svg>
543 </button>
544 <input type="number"
545 id="num-travelers-<?php echo esc_attr($item_id); ?>"
546 class="yatra-quantity-input-simple yatra-availability-num-travelers"
547 data-item="<?php echo esc_attr($item_id); ?>"
548 value="<?php echo esc_attr($simple_qty); ?>"
549 min="<?php echo esc_attr($simple_min_qty); ?>"
550 max="<?php echo esc_attr($simple_max_qty); ?>"
551 readonly
552 data-price="<?php echo esc_attr($display_sale_price); ?>">
553 <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' : ''; ?>>
554 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
555 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
556 </svg>
557 </button>
558 </div>
559 <span class="yatra-travelers-label"><?php esc_html_e('travelers', 'yatra'); ?></span>
560 </div>
561 <?php endif; ?>
562
563 <div class="yatra-card-action-help"><?php esc_html_e('Select travelers to update the total, then click Book Now.', 'yatra'); ?></div>
564 </div>
565 </div>
566
567 <!-- Booking Row at Bottom -->
568 <div class="yatra-card-booking-row">
569 <div class="yatra-card-total-box">
570 <div class="yatra-card-total-label"><?php esc_html_e('Total', 'yatra'); ?></div>
571 <div class="yatra-card-total-note" data-item="<?php echo esc_attr($item_id); ?>"><?php esc_html_e('for 1 traveler', 'yatra'); ?></div>
572 <div class="yatra-card-total-amount" data-item="<?php echo esc_attr($item_id); ?>" data-base-price="<?php echo esc_attr($initial_total_price); ?>">
573 <?php
574 $formatted_total = yatra_format_price($initial_total_price);
575 echo esc_html($formatted_total);
576 ?>
577 </div>
578 </div>
579 <?php
580 // Prepare price_types JSON for JavaScript
581 $price_types_json = '';
582 if (!empty($normalized_price_types)) {
583 $price_types_for_js = array_map(function($pt) {
584 $pt = (object) $pt;
585 return [
586 'category_id' => $pt->category_id ?? null,
587 'category_label' => $pt->category_label ?? $pt->label ?? '',
588 'original_price' => $pt->original_price ?? 0,
589 'sale_price' => $pt->sale_price ?? null,
590 'discounted_price' => $pt->discounted_price ?? null,
591 'effective_price' => $pt->effective_price ?? $pt->sale_price ?? $pt->discounted_price ?? $pt->original_price ?? 0,
592 'age_min' => $pt->age_min ?? null,
593 'age_max' => $pt->age_max ?? null,
594 ];
595 }, $normalized_price_types);
596 $price_types_json = wp_json_encode($price_types_for_js);
597 }
598
599 // Get departure time - prefer card data, fallback to from_date if it looks like time
600 $card_departure_time = '';
601 if (!empty($card['departure_time'])) {
602 $card_departure_time = $card['departure_time'];
603 } elseif (!empty($card['from_date']) && preg_match('/^\d{1,2}:\d{2}\s*(AM|PM)?$/i', $card['from_date'])) {
604 $card_departure_time = $card['from_date'];
605 }
606
607 // Is this a day trip?
608 $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']);
609 ?>
610 <?php if ($is_sold_out && !$waitlist_available): ?>
611 <?php // Sold out and no waitlist available: block booking here.
612 // The server would reject this at the final step
613 // (BookingSessionController → code 'sold_out'), so we
614 // disable the button instead of letting the customer go
615 // all the way through and fail. ?>
616 <button type="button"
617 class="yatra-card-book-btn yatra-card-book-btn--sold-out"
618 disabled
619 aria-disabled="true"
620 data-item="<?php echo esc_attr($item_id); ?>">
621 <span class="yatra-card-book-btn-text">
622 <span class="yatra-card-book-btn-title"><?php esc_html_e('Sold Out', 'yatra'); ?></span>
623 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('No seats available', 'yatra'); ?></span>
624 </span>
625 </button>
626 <?php else: ?>
627 <button type="button"
628 class="yatra-card-book-btn<?php echo ($is_sold_out && $waitlist_available) ? ' yatra-card-book-btn--waitlist' : ''; ?>"
629 data-trip-id="<?php echo esc_attr($trip_id); ?>"
630 data-availability-id="<?php echo esc_attr($item_id); ?>"
631 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
633 data-is-day-trip="<?php echo $is_card_day_trip ? '1' : '0'; ?>"
634 data-pricing-type="<?php echo esc_attr($card_pricing_type); ?>"
635 data-price-types="<?php echo esc_attr($price_types_json); ?>"
636 data-price="<?php echo esc_attr($display_sale_price); ?>"
637 data-item="<?php echo esc_attr($item_id); ?>">
638 <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?>
639 <span class="yatra-card-book-btn-text">
640 <?php if ($is_sold_out && $waitlist_available): ?>
641 <span class="yatra-card-book-btn-title"><?php esc_html_e('Join Waitlist', 'yatra'); ?></span>
642 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Sold out — join the waitlist', 'yatra'); ?></span>
643 <?php else: ?>
644 <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span>
645 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span>
646 <?php endif; ?>
647 </span>
648 </button>
649 <?php endif; ?>
650 </div>
651 </div>
652 </div>
653