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 +233 -81 3.0.2.6 → 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;
@@ -44,14 +57,85 @@
44 57 $sel_norm = $yatra_card_date_norm($selected_date_filter ?? '');
45 58 $card_date_norm = $yatra_card_date_norm($card['data_date'] ?? '');
46 59 $is_selected_card = ($sel_norm !== '' && $card_date_norm !== '' && $sel_norm === $card_date_norm);
47 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;
48 132 ?>
49 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); ?>"
50 134 data-availability-id="<?php echo esc_attr($item_id); ?>"
51 135 data-month="<?php echo esc_attr($card['data_month'] ?? ''); ?>"
52 136 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
53 - data-price="<?php echo esc_attr($sale_price); ?>"
137 + data-price="<?php echo esc_attr($display_sale_price); ?>"
54 138 data-seats="<?php echo esc_attr($seats_available); ?>"
55 139 data-item="<?php echo esc_attr($item_id); ?>"
56 140 data-index="<?php echo esc_attr($index); ?>">
57 141
@@ -57,25 +141,55 @@
57 141
58 142 <!-- Card Header (Clickable) -->
59 143 <div class="yatra-availability-card-header yatra-availability-toggle" role="button" tabindex="0" aria-label="<?php esc_attr_e('Toggle availability details', 'yatra'); ?>">
60 144
61 - <?php
62 - // Placeholder for discount badge (will be set after price calculation)
63 - $final_discount_badge = '';
64 - ?>
65 145 <?php if (!empty($card['date_display'])): ?>
66 146 <!-- Day Trip: Show date as header -->
67 147 <div class="yatra-card-date-header">
68 148 <?php echo yatra_svg_icon('calendar', 'yatra-icon-sm'); ?>
69 - <?php echo esc_html($card['date_display']); ?>
149 + <?php echo esc_html($date_display_display); ?>
70 150 </div>
71 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 +
72 186 <div class="yatra-card-header-grid <?php echo !empty($card['date_display']) ? 'yatra-day-trip-grid' : ''; ?>">
73 187
74 188 <!-- Start/Departure -->
75 189 <div class="yatra-card-header-item">
76 190 <div class="yatra-card-header-label"><?php echo esc_html($card['from_label'] ?? __('Departure', 'yatra')); ?></div>
77 - <div class="yatra-card-header-date"><?php echo esc_html($card['from_date'] ?? ''); ?></div>
191 + <div class="yatra-card-header-date"><?php echo esc_html($from_date_display); ?></div>
78 192 <div class="yatra-card-header-location"><?php echo esc_html($card['from_location'] ?? ''); ?></div>
79 193 </div>
80 194
81 195 <!-- End/Return -->
@@ -80,9 +194,9 @@
80 194
81 195 <!-- End/Return -->
82 196 <div class="yatra-card-header-item">
83 197 <div class="yatra-card-header-label"><?php echo esc_html($card['to_label'] ?? __('Return', 'yatra')); ?></div>
84 - <div class="yatra-card-header-date"><?php echo esc_html($card['to_date'] ?? ''); ?></div>
198 + <div class="yatra-card-header-date"><?php echo esc_html($to_date_display); ?></div>
85 199 <div class="yatra-card-header-location"><?php echo esc_html($card['to_location'] ?? ''); ?></div>
86 200 </div>
87 201
88 202 <!-- Seats -->
@@ -91,26 +205,16 @@
91 205 <div class="yatra-card-header-seats <?php echo $is_limited ? 'limited' : ''; ?>"><?php echo esc_html($card['seats'] ?? '0'); ?></div>
92 206 <div class="yatra-card-header-sub"><?php esc_html_e('per departure', 'yatra'); ?></div>
93 207 </div>
94 208
95 - <!-- Price -->
96 - <div class="yatra-card-header-price">
97 - <div class="yatra-card-price-group">
98 - <?php
99 - // Pricing is already calculated with dynamic pricing in Controller
100 - $display_sale_price = $sale_price;
101 - $display_original_price = $original_price;
102 -
103 - // Show discount badge if there's a discount
104 - $final_discount_badge = '';
105 - if (!empty($card['discount_text'])) {
106 - $final_discount_badge = '<div class="yatra-card-discount-badge yatra-badge-discount">' . esc_html($card['discount_text']) . '</div>';
107 - }
108 -
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
109 213 echo '<span class="yatra-sale-price">' . yatra_format_price($display_sale_price) . '</span>';
110 214 ?>
111 215 </div>
112 - <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>
113 217 </div>
114 218
115 219 <!-- Toggle Arrow -->
116 220 <div class="yatra-card-header-arrow">
@@ -118,39 +222,8 @@
118 222 <path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
119 223 </svg>
120 224 </div>
121 225 </div>
122 -
123 - <!-- Badges Container (Status + Discount) -->
124 - <div class="yatra-card-badges-container">
125 - <!-- Status Badge -->
126 - <?php
127 - $card_status = $card['status'] ?? 'available';
128 - $is_sold_out = isset($card['is_sold_out']) && $card['is_sold_out'];
129 -
130 - // Determine status display
131 - if ($is_sold_out || $card_status === 'sold_out') {
132 - echo '<div class="yatra-badge-status yatra-badge-sold-out">' . esc_html__('Sold Out', 'yatra') . '</div>';
133 - } elseif ($card_status === 'limited') {
134 - echo '<div class="yatra-badge-status yatra-badge-limited">' . esc_html__('Limited', 'yatra') . '</div>';
135 - } elseif ($card_status === 'blocked') {
136 - echo '<div class="yatra-badge-status yatra-badge-blocked">' . esc_html__('Blocked', 'yatra') . '</div>';
137 - } elseif ($card_status === 'closed') {
138 - echo '<div class="yatra-badge-status yatra-badge-closed">' . esc_html__('Closed', 'yatra') . '</div>';
139 - } elseif ($card_status === 'cancelled') {
140 - echo '<div class="yatra-badge-status yatra-badge-cancelled">' . esc_html__('Cancelled', 'yatra') . '</div>';
141 - } else {
142 - echo '<div class="yatra-badge-status yatra-badge-available">' . esc_html__('Available', 'yatra') . '</div>';
143 - }
144 - ?>
145 -
146 - <!-- Discount Badge -->
147 - <?php
148 - if (!empty($final_discount_badge)) {
149 - echo $final_discount_badge;
150 - }
151 - ?>
152 - </div>
153 226 </div>
154 227
155 228 <!-- Card Body (Expandable) -->
156 229 <div class="yatra-availability-card-body">
@@ -163,10 +236,22 @@
163 236 <div class="yatra-card-info-content">
164 237 <div class="yatra-card-info-label"><?php esc_html_e('Duration', 'yatra'); ?></div>
165 238 <div class="yatra-card-info-value">
166 239 <?php
167 - $duration_days = (int) ($trip_data->duration_days ?? 1);
168 - 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 + }
169 254 ?>
170 255 </div>
171 256 </div>
172 257 </div>
@@ -176,9 +261,12 @@
176 261 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
177 262 </div>
178 263 <div class="yatra-card-info-content">
179 264 <div class="yatra-card-info-label"><?php esc_html_e('Seats left', 'yatra'); ?></div>
180 - <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>
181 269 </div>
182 270 </div>
183 271
184 272 <?php
@@ -199,9 +287,10 @@
199 287 if (preg_match('/(\d+)\s*(hour|day|week)s?\s*before/i', $cancellation_policy, $matches)) {
200 288 $time_value = $matches[1];
201 289 $time_unit = $matches[2];
202 290 $cancellation_value = sprintf(
203 - __('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'),
204 293 $time_value,
205 294 $time_unit,
206 295 $time_value > 1 ? 's' : ''
207 296 );
@@ -236,9 +325,8 @@
236 325 <?php
237 326 // Use card-level pricing type as source of truth
238 327 $card_pricing_type = $card['pricing_type'] ?? $pricing_type ?? 'regular';
239 328 $card_price_types = [];
240 - $dp_enabled = apply_filters('yatra_dynamic_pricing_enabled', false);
241 329
242 330 // Use availability-specific traveler_pricing for traveler-based pricing
243 331 if ($card_pricing_type === 'traveler_based' && !empty($card['traveler_pricing'])) {
244 332 $card_price_types = $card['traveler_pricing'];
@@ -268,8 +356,21 @@
268 356 $pt_category_id = $pt->category_id ?? $pt_index;
269 357 $pt_default = $pt_index === 0 ? 1 : 0;
270 358 $pt_min_qty = 0;
271 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 + }
272 373
273 374 // Determine price with fallback chain
274 375 $pt_price = 0;
275 376 if (isset($pt->effective_price) && $pt->effective_price > 0) {
@@ -281,13 +382,20 @@
281 382 }
282 383
283 384 // Apply dynamic pricing to traveler category prices
284 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 + }
285 391 $pt_price = apply_filters('yatra_availability_price', $pt_price, $trip_id, [
286 392 'departure_date' => $card['date'] ?? null,
287 393 'spots_remaining' => $card['spots_remaining'] ?? null,
288 394 'availability_id' => $item_id,
289 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,
290 398 ]);
291 399 }
292 400
293 401 // Age info
@@ -295,12 +403,15 @@
295 403 $pt_age_min = isset($pt->age_min) ? (int) $pt->age_min : null;
296 404 $pt_age_max = isset($pt->age_max) ? (int) $pt->age_max : null;
297 405 if ($pt_age_min !== null || $pt_age_max !== null) {
298 406 if ($pt_age_min !== null && $pt_age_max !== null) {
299 - $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);
300 409 } elseif ($pt_age_min !== null) {
410 + /* translators: %d: minimum age. */
301 411 $pt_age_text = sprintf(__('(Age %d+)', 'yatra'), $pt_age_min);
302 412 } else {
413 + /* translators: %d: maximum age. */
303 414 $pt_age_text = sprintf(__('(Up to age %d)', 'yatra'), $pt_age_max);
304 415 }
305 416 }
306 417
@@ -307,12 +418,15 @@
307 418 // Build pricing label for per_group mode
308 419 $pricing_label = '';
309 420 if ($pt_is_per_group) {
310 421 if (!empty($pt->min_pax) && !empty($pt->max_pax)) {
311 - $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);
312 424 } elseif (!empty($pt->max_pax)) {
425 + /* translators: %d: maximum pax for the group price. */
313 426 $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax);
314 427 } elseif (!empty($pt->min_pax)) {
428 + /* translators: %d: minimum pax for the group price. */
315 429 $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax);
316 430 } else {
317 431 $pricing_label = __('per group', 'yatra');
318 432 }
@@ -338,8 +452,10 @@
338 452 'row_attrs' => [
339 453 'data-category-id' => $pt_category_id,
340 454 'data-price' => $pt_price,
341 455 'data-pricing-mode' => $pt_pricing_mode,
456 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
457 + 'data-max-pax' => $pt->max_pax ?? '',
342 458 ],
343 459 'minus_disabled' => $pt_default <= $pt_min_qty,
344 460 'plus_disabled' => $pt_default >= $pt_max_qty,
345 461 'minus_attrs' => [
@@ -345,8 +461,9 @@
345 461 'minus_attrs' => [
346 462 'data-target' => $input_id,
347 463 'data-item' => $item_id,
348 464 'data-category' => $pt_category_id,
465 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
349 466 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label),
350 467 ],
351 468 'plus_attrs' => [
352 469 'data-target' => $input_id,
@@ -351,8 +468,9 @@
351 468 'plus_attrs' => [
352 469 'data-target' => $input_id,
353 470 'data-item' => $item_id,
354 471 'data-category' => $pt_category_id,
472 + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */
355 473 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label),
356 474 ],
357 475 'input_attrs' => [
358 476 'id' => $input_id,
@@ -364,8 +482,10 @@
364 482 'data-category' => $pt_category_id,
365 483 'data-category-label' => $pt_label,
366 484 'data-price' => $pt_price,
367 485 'data-pricing-mode' => $pt_pricing_mode,
486 + 'data-group-overflow' => $pt->group_overflow ?? 'block',
487 + 'data-max-pax' => $pt->max_pax ?? '',
368 488 ],
369 489 ];
370 490
371 491 if ($pt_default > 0) {
@@ -401,8 +521,17 @@
401 521 include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php';
402 522 ?>
403 523 <?php else: ?>
404 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 + ?>
405 534 <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>">
406 535 <div class="yatra-booking-field-icon">
407 536 <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?>
408 537 </div>
@@ -411,18 +540,18 @@
411 540 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
412 541 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
413 542 </svg>
414 543 </button>
415 - <input type="number"
416 - id="num-travelers-<?php echo esc_attr($item_id); ?>"
417 - 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"
418 547 data-item="<?php echo esc_attr($item_id); ?>"
419 - value="1"
420 - min="1"
421 - 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); ?>"
422 551 readonly
423 - data-price="<?php echo esc_attr($sale_price); ?>">
424 - <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' : ''; ?>>
425 554 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
426 555 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
427 556 </svg>
428 557 </button>
@@ -477,10 +606,27 @@
477 606
478 607 // Is this a day trip?
479 608 $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']);
480 609 ?>
481 - <button type="button"
482 - 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' : ''; ?>"
483 629 data-trip-id="<?php echo esc_attr($trip_id); ?>"
484 630 data-availability-id="<?php echo esc_attr($item_id); ?>"
485 631 data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>"
486 632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
@@ -486,15 +632,21 @@
486 632 data-departure-time="<?php echo esc_attr($card_departure_time); ?>"
487 633 data-is-day-trip="<?php echo $is_card_day_trip ? '1' : '0'; ?>"
488 634 data-pricing-type="<?php echo esc_attr($card_pricing_type); ?>"
489 635 data-price-types="<?php echo esc_attr($price_types_json); ?>"
490 - data-price="<?php echo esc_attr($sale_price); ?>"
636 + data-price="<?php echo esc_attr($display_sale_price); ?>"
491 637 data-item="<?php echo esc_attr($item_id); ?>">
492 638 <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?>
493 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: ?>
494 644 <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span>
495 645 <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span>
646 + <?php endif; ?>
496 647 </span>
497 648 </button>
649 + <?php endif; ?>
498 650 </div>
499 651 </div>
500 652 </div>