| @@ -27,8 +27,29 @@ | ||
| 27 | 27 | if ($is_sold_out || $card_status === 'sold_out') { |
| 28 | 28 | $card_status = 'sold_out'; |
| 29 | 29 | } |
| 30 | 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 | + | |
| 31 | 52 | // Match Y-m-d even if DB returns datetime (e.g. 2026-08-13 00:00:00) |
| 32 | 53 | $yatra_card_date_norm = static function ($v): string { |
| 33 | 54 | $v = trim((string) $v); |
| 34 | 55 | return preg_match('/^(\d{4}-\d{2}-\d{2})/', $v, $m) ? $m[1] : $v; |
| @@ -185,15 +206,15 @@ | ||
| 185 | 206 | <div class="yatra-card-header-sub"><?php esc_html_e('per departure', 'yatra'); ?></div> |
| 186 | 207 | </div> |
| 187 | 208 | |
| 188 | 209 | <!-- Price (sale only; struck original + % badge sit in .yatra-card-badges-container with status) --> |
| 189 | - <div class="yatra-card-header-price"> | |
| 210 | + <div class="yatra-card-header-price" data-price-unit="<?php echo esc_attr($card['price_unit'] ?? 'per_person'); ?>"> | |
| 190 | 211 | <div class="yatra-card-price-group yatra-card-price-group--stack"> |
| 191 | 212 | <?php |
| 192 | 213 | echo '<span class="yatra-sale-price">' . yatra_format_price($display_sale_price) . '</span>'; |
| 193 | 214 | ?> |
| 194 | 215 | </div> |
| 195 | - <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> | |
| 196 | 217 | </div> |
| 197 | 218 | |
| 198 | 219 | <!-- Toggle Arrow --> |
| 199 | 220 | <div class="yatra-card-header-arrow"> |
| @@ -215,10 +236,22 @@ | ||
| 215 | 236 | <div class="yatra-card-info-content"> |
| 216 | 237 | <div class="yatra-card-info-label"><?php esc_html_e('Duration', 'yatra'); ?></div> |
| 217 | 238 | <div class="yatra-card-info-value"> |
| 218 | 239 | <?php |
| 219 | - $duration_days = (int) ($trip_data->duration_days ?? 1); | |
| 220 | - 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 | + } | |
| 221 | 254 | ?> |
| 222 | 255 | </div> |
| 223 | 256 | </div> |
| 224 | 257 | </div> |
| @@ -330,8 +363,14 @@ | ||
| 330 | 363 | if ($pt_is_per_group && !empty($pt->max_pax) |
| 331 | 364 | && (($pt->group_overflow ?? 'block') !== 'per_block')) { |
| 332 | 365 | $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax); |
| 333 | 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 | + } | |
| 334 | 373 | |
| 335 | 374 | // Determine price with fallback chain |
| 336 | 375 | $pt_price = 0; |
| 337 | 376 | if (isset($pt->effective_price) && $pt->effective_price > 0) { |
| @@ -482,8 +521,17 @@ | ||
| 482 | 521 | include YATRA_PLUGIN_PATH . 'templates/partials/traveler-selector.php'; |
| 483 | 522 | ?> |
| 484 | 523 | <?php else: ?> |
| 485 | 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 | + ?> | |
| 486 | 534 | <div class="yatra-booking-travelers-simple yatra-availability-travelers-simple" data-item="<?php echo esc_attr($item_id); ?>"> |
| 487 | 535 | <div class="yatra-booking-field-icon"> |
| 488 | 536 | <?php echo yatra_svg_icon('users', 'yatra-icon-sm'); ?> |
| 489 | 537 | </div> |
| @@ -492,18 +540,18 @@ | ||
| 492 | 540 | <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 493 | 541 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/> |
| 494 | 542 | </svg> |
| 495 | 543 | </button> |
| 496 | - <input type="number" | |
| 497 | - id="num-travelers-<?php echo esc_attr($item_id); ?>" | |
| 498 | - 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" | |
| 499 | 547 | data-item="<?php echo esc_attr($item_id); ?>" |
| 500 | - value="1" | |
| 501 | - min="1" | |
| 502 | - 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); ?>" | |
| 503 | 551 | readonly |
| 504 | 552 | data-price="<?php echo esc_attr($display_sale_price); ?>"> |
| 505 | - <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'); ?>"> | |
| 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' : ''; ?>> | |
| 506 | 554 | <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 507 | 555 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/> |
| 508 | 556 | </svg> |
| 509 | 557 | </button> |
| @@ -558,10 +606,27 @@ | ||
| 558 | 606 | |
| 559 | 607 | // Is this a day trip? |
| 560 | 608 | $is_card_day_trip = !empty($card['is_day_trip']) || !empty($card['date_display']); |
| 561 | 609 | ?> |
| 562 | - <button type="button" | |
| 563 | - 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' : ''; ?>" | |
| 564 | 629 | data-trip-id="<?php echo esc_attr($trip_id); ?>" |
| 565 | 630 | data-availability-id="<?php echo esc_attr($item_id); ?>" |
| 566 | 631 | data-date="<?php echo esc_attr($card['data_date'] ?? ''); ?>" |
| 567 | 632 | data-departure-time="<?php echo esc_attr($card_departure_time); ?>" |
| @@ -571,11 +636,17 @@ | ||
| 571 | 636 | data-price="<?php echo esc_attr($display_sale_price); ?>" |
| 572 | 637 | data-item="<?php echo esc_attr($item_id); ?>"> |
| 573 | 638 | <?php echo yatra_svg_icon('shopping-cart', 'yatra-icon-sm'); ?> |
| 574 | 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: ?> | |
| 575 | 644 | <span class="yatra-card-book-btn-title"><?php esc_html_e('Book Now', 'yatra'); ?></span> |
| 576 | 645 | <span class="yatra-card-book-btn-subtitle"><?php esc_html_e('Continue to booking', 'yatra'); ?></span> |
| 646 | + <?php endif; ?> | |
| 577 | 647 | </span> |
| 578 | 648 | </button> |
| 649 | + <?php endif; ?> | |
| 579 | 650 | </div> |
| 580 | 651 | </div> |
| 581 | 652 | </div> |