| @@ -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> |