PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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
← All changes | templates/partials/availability-card.php +213 -78 3.0.2.9 → 3.0.16 View file →
@@ -17,18 +17,10 @@
17 17 // Get pricing from card (already calculated with dynamic pricing in Controller)
18 18 $sale_price = (float) ($card['sale_price'] ?? 0);
19 19 $original_price = (float) ($card['original_price'] ?? 0);
20 20
21 - // For traveler-based pricing, use first category price for initial total
22 21 $initial_total_price = $sale_price;
23 - if (!empty($card['pricing_type']) && $card['pricing_type'] === 'traveler_based' && !empty($card['traveler_pricing'])) {
24 - $first_traveler = is_array($card['traveler_pricing']) ? $card['traveler_pricing'][0] : null;
25 - if ($first_traveler) {
26 - $first_traveler = is_array($first_traveler) ? (object) $first_traveler : $first_traveler;
27 - $initial_total_price = (float) ($first_traveler->effective_price ?? $first_traveler->discounted_price ?? $first_traveler->original_price ?? $sale_price);
28 - }
29 - }
30 -
22 +
31 23 $seats_available = (int) ($card['seats_available'] ?? 0);
32 24 $is_limited = !empty($card['is_limited']);
33 25 $card_status = $card['status'] ?? 'available';
34 26 $is_sold_out = isset($card['is_sold_out']) && $card['is_sold_out'];
@@ -35,8 +27,29 @@
35 27 if ($is_sold_out || $card_status === 'sold_out') {
36 28 $card_status = 'sold_out';
37 29 }
38 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 +
39 52 // Match Y-m-d even if DB returns datetime (e.g. 2026-08-13 00:00:00)
40 53 $yatra_card_date_norm = static function ($v): string {
41 54 $v = trim((string) $v);
42 55 return preg_match('/^(\d{4}-\d{2}-\d{2})/', $v, $m) ? $m[1] : $v;
@@ -61,14 +74,68 @@
61 74
62 75 $from_date_display = $yatra_format_card_date_for_display($card['from_date'] ?? '');
63 76 $to_date_display = $yatra_format_card_date_for_display($card['to_date'] ?? '');
64 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;
65 132 ?>
66 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); ?>"
67 134 data-availability-id="<?php echo esc_attr($item_id); ?>"
68 135 data-month="<?php echo esc_attr($card['data_month'] ?? ''); ?>"
69 136 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
70 - data-price="<?php echo esc_attr($sale_price); ?>"
137 + data-price="<?php echo esc_attr($display_sale_price); ?>"
71 138 data-seats="<?php echo esc_attr($seats_available); ?>"
72 139 data-item="<?php echo esc_attr($item_id); ?>"
73 140 data-index="<?php echo esc_attr($index); ?>">
74 141
@@ -74,12 +141,8 @@
74 141
75 142 <!-- Card Header (Clickable) -->
76 143 <div class="yatra-availability-card-header yatra-availability-toggle" role="button" tabindex="0" aria-label="<?php esc_attr_e('Toggle availability details', 'yatra'); ?>">
77 144
78 - <?php
79 - // Placeholder for discount badge (will be set after price calculation)
80 - $final_discount_badge = '';
81 - ?>
82 145 <?php if (!empty($card['date_display'])): ?>
83 146 <!-- Day Trip: Show date as header -->
84 147 <div class="yatra-card-date-header">
85 148 <?php echo yatra_svg_icon('calendar', 'yatra-icon-sm'); ?>
@@ -85,8 +148,42 @@
85 148 <?php echo yatra_svg_icon('calendar', 'yatra-icon-sm'); ?>
86 149 <?php echo esc_html($date_display_display); ?>
87 150 </div>
88 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 +
89 186 <div class="yatra-card-header-grid <?php echo !empty($card['date_display']) ? 'yatra-day-trip-grid' : ''; ?>">
90 187
91 188 <!-- Start/Departure -->
92 189 <div class="yatra-card-header-item">
@@ -108,26 +205,16 @@
108 205 <div class="yatra-card-header-seats <?php echo $is_limited ? 'limited' : ''; ?>"><?php echo esc_html($card['seats'] ?? '0'); ?></div>
109 206 <div class="yatra-card-header-sub"><?php esc_html_e('per departure', 'yatra'); ?></div>
110 207 </div>
111 208
112 - <!-- Price -->
113 - <div class="yatra-card-header-price">
114 - <div class="yatra-card-price-group">
115 - <?php
116 - // Pricing is already calculated with dynamic pricing in Controller
117 - $display_sale_price = $sale_price;
118 - $display_original_price = $original_price;
119 -
120 - // Show discount badge if there's a discount
121 - $final_discount_badge = '';
122 - if (!empty($card['discount_text'])) {
123 - $final_discount_badge = '<div class="yatra-card-discount-badge yatra-badge-discount">' . esc_html($card['discount_text']) . '</div>';
124 - }
125 -
209 + <!-- Price (sale only; struck original + % badge sit in .yatra-card-badges-container with status) -->
210 + <div class="yatra-card-header-price" data-price-unit="<?php echo esc_attr($card['price_unit'] ?? 'per_person'); ?>">
211 + <div class="yatra-card-price-group yatra-card-price-group--stack">
212 + <?php
126 213 echo '<span class="yatra-sale-price">' . yatra_format_price($display_sale_price) . '</span>';
127 214 ?>
128 215 </div>
129 - <span class="yatra-price-label"><?php echo esc_html__('per person', 'yatra'); ?></span>
216 + <span class="yatra-price-label"><?php echo esc_html($card['price_unit_label'] ?? __('per person', 'yatra')); ?></span>
130 217 </div>
131 218
132 219 <!-- Toggle Arrow -->
133 220 <div class="yatra-card-header-arrow">
@@ -135,39 +222,8 @@
135 222 <path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
136 223 </svg>
137 224 </div>
138 225 </div>
139 -
140 - <!-- Badges Container (Status + Discount) -->
141 - <div class="yatra-card-badges-container">
142 - <!-- Status Badge -->
143 - <?php
144 - $card_status = $card['status'] ?? 'available';
145 - $is_sold_out = isset($card['is_sold_out']) && $card['is_sold_out'];
146 -
147 - // Determine status display
148 - if ($is_sold_out || $card_status === 'sold_out') {
149 - echo '<div class="yatra-badge-status yatra-badge-sold-out">' . esc_html__('Sold Out', 'yatra') . '</div>';
150 - } elseif ($card_status === 'limited') {
151 - echo '<div class="yatra-badge-status yatra-badge-limited">' . esc_html__('Limited', 'yatra') . '</div>';
152 - } elseif ($card_status === 'blocked') {
153 - echo '<div class="yatra-badge-status yatra-badge-blocked">' . esc_html__('Blocked', 'yatra') . '</div>';
154 - } elseif ($card_status === 'closed') {
155 - echo '<div class="yatra-badge-status yatra-badge-closed">' . esc_html__('Closed', 'yatra') . '</div>';
156 - } elseif ($card_status === 'cancelled') {
157 - echo '<div class="yatra-badge-status yatra-badge-cancelled">' . esc_html__('Cancelled', 'yatra') . '</div>';
158 - } else {
159 - echo '<div class="yatra-badge-status yatra-badge-available">' . esc_html__('Available', 'yatra') . '</div>';
160 - }
161 - ?>
162 -
163 - <!-- Discount Badge -->
164 - <?php
165 - if (!empty($final_discount_badge)) {
166 - echo $final_discount_badge;
167 - }
168 - ?>
169 - </div>
170 226 </div>
171 227
172 228 <!-- Card Body (Expandable) -->
173 229 <div class="yatra-availability-card-body">
@@ -180,10 +236,22 @@
180 236 <div class="yatra-card-info-content">
181 237 <div class="yatra-card-info-label"><?php esc_html_e('Duration', 'yatra'); ?></div>
182 238 <div class="yatra-card-info-value">
183 239 <?php
184 - $duration_days = (int) ($trip_data->duration_days ?? 1);
185 - echo esc_html($duration_days . ' ' . _n('Day', 'Days', $duration_days, 'yatra'));
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 + }
186 254 ?>
187 255 </div>
188 256 </div>
189 257 </div>
@@ -193,9 +261,12 @@
193 261 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
194 262 </div>
195 263 <div class="yatra-card-info-content">
196 264 <div class="yatra-card-info-label"><?php esc_html_e('Seats left', 'yatra'); ?></div>
197 - <div class="yatra-card-info-value"><?php echo esc_html(sprintf(_n('%d seat', '%d seats', $seats_available, 'yatra'), $seats_available)); ?></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>
198 269 </div>
199 270 </div>
200 271
201 272 <?php
@@ -216,9 +287,10 @@
216 287 if (preg_match('/(\d+)\s*(hour|day|week)s?\s*before/i', $cancellation_policy, $matches)) {
217 288 $time_value = $matches[1];
218 289 $time_unit = $matches[2];
219 290 $cancellation_value = sprintf(
220 - __('Up to %d %s%s before', 'yatra'),
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'),
221 293 $time_value,
222 294 $time_unit,
223 295 $time_value > 1 ? 's' : ''
224 296 );
@@ -253,9 +325,8 @@
253 325 <?php
254 326 // Use card-level pricing type as source of truth
255 327 $card_pricing_type = $card['pricing_type'] ?? $pricing_type ?? 'regular';
256 328 $card_price_types = [];
257 - $dp_enabled = apply_filters('yatra_dynamic_pricing_enabled', false);
258 329
259 330 // Use availability-specific traveler_pricing for traveler-based pricing
260 331 if ($card_pricing_type === 'traveler_based' && !empty($card['traveler_pricing'])) {
261 332 $card_price_types = $card['traveler_pricing'];
@@ -285,8 +356,21 @@
285 356 $pt_category_id = $pt->category_id ?? $pt_index;
286 357 $pt_default = $pt_index === 0 ? 1 : 0;
287 358 $pt_min_qty = 0;
288 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 + }
289 373
290 374 // Determine price with fallback chain
291 375 $pt_price = 0;
292 376 if (isset($pt->effective_price) && $pt->effective_price > 0) {
@@ -298,13 +382,20 @@
298 382 }
299 383
300 384 // Apply dynamic pricing to traveler category prices
301 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 + }
302 391 $pt_price = apply_filters('yatra_availability_price', $pt_price, $trip_id, [
303 392 'departure_date' => $card['date'] ?? null,
304 393 'spots_remaining' => $card['spots_remaining'] ?? null,
305 394 'availability_id' => $item_id,
306 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,
307 398 ]);
308 399 }
309 400
310 401 // Age info
@@ -312,12 +403,15 @@
312 403 $pt_age_min = isset($pt->age_min) ? (int) $pt->age_min : null;
313 404 $pt_age_max = isset($pt->age_max) ? (int) $pt->age_max : null;
314 405 if ($pt_age_min !== null || $pt_age_max !== null) {
315 406 if ($pt_age_min !== null && $pt_age_max !== null) {
316 - $pt_age_text = sprintf(__('(Age %d-%d)', 'yatra'), $pt_age_min, $pt_age_max);
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);
317 409 } elseif ($pt_age_min !== null) {
410 + /* translators: %d: minimum age. */
318 411 $pt_age_text = sprintf(__('(Age %d+)', 'yatra'), $pt_age_min);
319 412 } else {
413 + /* translators: %d: maximum age. */
320 414 $pt_age_text = sprintf(__('(Up to age %d)', 'yatra'), $pt_age_max);
321 415 }
322 416 }
323 417
@@ -324,12 +418,15 @@
324 418 // Build pricing label for per_group mode
325 419 $pricing_label = '';
326 420 if ($pt_is_per_group) {
327 421 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
328 - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $pt->min_pax, $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);
329 424 } elseif (!empty($pt->max_pax)) {
425 + /* translators: %d: maximum pax for the group price. */
330 426 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
331 427 } elseif (!empty($pt->min_pax)) {
428 + /* translators: %d: minimum pax for the group price. */
332 429 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
333 430 } else {
334 431 $pricing_label = __('per group', 'yatra');
335 432 }
@@ -355,8 +452,10 @@
355 452 'row_attrs' => [
356 453 'data-category-id' => $pt_category_id,
357 454 'data-price' => $pt_price,
358 455 'data-pricing-mode' => $pt_pricing_mode,
456 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
457 + 'data-max-pax' => $pt->max_pax ?? '',
359 458 ],
360 459 'minus_disabled' => $pt_default <= $pt_min_qty,
361 460 'plus_disabled' => $pt_default >= $pt_max_qty,
362 461 'minus_attrs' => [
@@ -362,8 +461,9 @@
362 461 'minus_attrs' => [
363 462 'data-target' => $input_id,
364 463 'data-item' => $item_id,
365 464 'data-category' => $pt_category_id,
465 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
366 466 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
367 467 ],
368 468 'plus_attrs' => [
369 469 'data-target' => $input_id,
@@ -368,8 +468,9 @@
368 468 'plus_attrs' => [
369 469 'data-target' => $input_id,
370 470 'data-item' => $item_id,
371 471 'data-category' => $pt_category_id,
472 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
372 473 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
373 474 ],
374 475 'input_attrs' => [
375 476 'id' => $input_id,
@@ -381,8 +482,10 @@
381 482 'data-category' => $pt_category_id,
382 483 'data-category-label' => $pt_label,
383 484 'data-price' => $pt_price,
384 485 'data-pricing-mode' => $pt_pricing_mode,
486 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
487 + 'data-max-pax' => $pt->max_pax ?? '',
385 488 ],
386 489 ];
387 490
388 491 if ($pt_default > 0) {
@@ -418,8 +521,17 @@
418 521 include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php';
419 522 ?>
420 523 <?php else: ?>
421 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 + ?>
422 534 <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>">
423 535 <div class="yatra-booking-field-icon">
424 536 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
425 537 </div>
@@ -428,18 +540,18 @@
428 540 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
429 541 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
430 542 </svg>
431 543 </button>
432 - <input type="number"
433 - id="num-travelers-<?php echo esc_attr($item_id); ?>"
434 - class="yatra-quantity-input-simple yatra-availability-num-travelers"
544 + <input type="number"
545 + id="num-travelers-<?php echo esc_attr($item_id); ?>"
546 + class="yatra-quantity-input-simple yatra-availability-num-travelers"
435 547 data-item="<?php echo esc_attr($item_id); ?>"
436 - value="1"
437 - min="1"
438 - max="<?php echo esc_attr(min($seats_available, $max_travelers)); ?>"
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); ?>"
439 551 readonly
440 - data-price="<?php echo esc_attr($sale_price); ?>">
441 - <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'); ?>">
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' : ''; ?>>
442 554 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
443 555 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
444 556 </svg>
445 557 </button>
@@ -494,10 +606,27 @@
494 606
495 607 // Is this a day trip?
496 608 $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']);
497 609 ?>
498 - <button type="button"
499 - class="yatra-card-book-btn"
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' : ''; ?>"
500 629 data-trip-id="<?php echo esc_attr($trip_id); ?>"
501 630 data-availability-id="<?php echo esc_attr($item_id); ?>"
502 631 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
503 632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
@@ -503,15 +632,21 @@
503 632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
504 633 data-is-day-trip="<?php echo $is_card_day_trip ? '1' : '0'; ?>"
505 634 data-pricing-type="<?php echo esc_attr($card_pricing_type); ?>"
506 635 data-price-types="<?php echo esc_attr($price_types_json); ?>"
507 - data-price="<?php echo esc_attr($sale_price); ?>"
636 + data-price="<?php echo esc_attr($display_sale_price); ?>"
508 637 data-item="<?php echo esc_attr($item_id); ?>">
509 638 <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?>
510 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: ?>
511 644 <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span>
512 645 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span>
646 + <?php endif; ?>
513 647 </span>
514 648 </button>
649 + <?php endif; ?>
515 650 </div>
516 651 </div>
517 652 </div>