| @@ -100,10 +100,17 @@ | ||
| 100 | 100 | $slug |
| 101 | 101 | ) |
| 102 | 102 | ); |
| 103 | 103 | |
| 104 | - // If trip exists but admin is not logged in, return null to show "not found" | |
| 105 | - if ($existingTrip && !current_user_can('yatra_edit_trips')) { | |
| 104 | + // If trip exists but viewer can't preview drafts, hide it. | |
| 105 | + // Admin fallback so site owners always see drafts even when | |
| 106 | + // the Team module isn't active and yatra_edit_trips isn't | |
| 107 | + // on the admin role. | |
| 108 | + if ( | |
| 109 | + $existingTrip | |
| 110 | + && !current_user_can('manage_options') | |
| 111 | + && !current_user_can('yatra_edit_trips') | |
| 112 | + ) { | |
| 106 | 113 | return null; |
| 107 | 114 | } |
| 108 | 115 | } |
| 109 | 116 | |
| @@ -389,13 +396,14 @@ | ||
| 389 | 396 | { |
| 390 | 397 | // Use centralized AvailabilityResolutionService |
| 391 | 398 | $resolutionService = new \Yatra\Services\AvailabilityResolutionService(); |
| 392 | 399 | |
| 393 | - // Get dates for next 12 months | |
| 400 | + // From today up to the configurable booking horizon (Settings → Booking; | |
| 401 | + // 12 months unless changed — the previous hard-coded value). | |
| 394 | 402 | $fromDate = date('Y-m-d'); |
| 395 | - $toDate = date('Y-m-d', strtotime('+12 months')); | |
| 403 | + $toDate = yatra_get_availability_horizon_date($fromDate); | |
| 396 | 404 | |
| 397 | - $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate); | |
| 405 | + $availability = $resolutionService->getAllAvailabilityDates($trip_id, $fromDate, $toDate, \Yatra\Services\SettingsService::isEnabled('show_sold_out')); | |
| 398 | 406 | |
| 399 | 407 | // Add calculated fields |
| 400 | 408 | foreach ($availability as $avail) { |
| 401 | 409 | // Calculate if limited availability |
| @@ -459,8 +467,9 @@ | ||
| 459 | 467 | 'age_min' => isset($meta['age_min']) ? (int) $meta['age_min'] : null, |
| 460 | 468 | 'age_max' => isset($meta['age_max']) ? (int) $meta['age_max'] : null, |
| 461 | 469 | 'min_pax' => isset($meta['min_pax']) ? (int) $meta['min_pax'] : null, |
| 462 | 470 | 'max_pax' => isset($meta['max_pax']) ? (int) $meta['max_pax'] : null, |
| 471 | + 'group_overflow' => isset($meta['group_overflow']) && in_array($meta['group_overflow'], ['block', 'per_block'], true) ? $meta['group_overflow'] : 'block', | |
| 463 | 472 | 'max_quantity' => isset($meta['max_quantity']) ? (int) $meta['max_quantity'] : null, |
| 464 | 473 | 'description' => $meta['description'] ?? '', |
| 465 | 474 | ]; |
| 466 | 475 | } |
| @@ -490,8 +499,9 @@ | ||
| 490 | 499 | 'age_min' => $cat ? $cat->age_min : null, |
| 491 | 500 | 'age_max' => $cat ? $cat->age_max : null, |
| 492 | 501 | 'min_pax' => $cat ? $cat->min_pax : null, |
| 493 | 502 | 'max_pax' => $cat ? $cat->max_pax : null, |
| 503 | + 'group_overflow' => $cat ? $cat->group_overflow : ($pt['group_overflow'] ?? 'block'), | |
| 494 | 504 | 'max_quantity' => $cat ? $cat->max_quantity : null, |
| 495 | 505 | 'description' => $cat ? $cat->description : ($pt['description'] ?? ''), |
| 496 | 506 | ]; |
| 497 | 507 | } |
| @@ -1085,9 +1095,13 @@ | ||
| 1085 | 1095 | $patterns = [ |
| 1086 | 1096 | '/youtube\.com\/watch\?v=([^&]+)/', |
| 1087 | 1097 | '/youtube\.com\/embed\/([^?]+)/', |
| 1088 | 1098 | '/youtu\.be\/([^?]+)/', |
| 1089 | - '/youtube\.com\/v\/([^?]+)/' | |
| 1099 | + '/youtube\.com\/v\/([^?]+)/', | |
| 1100 | + // Shorts + live URLs (e.g. youtube.com/shorts/XFR9Ti-4RbM?si=...). | |
| 1101 | + // ID stops at ?, & or / so trailing query params are excluded. | |
| 1102 | + '/youtube\.com\/shorts\/([^?&\/]+)/', | |
| 1103 | + '/youtube\.com\/live\/([^?&\/]+)/' | |
| 1090 | 1104 | ]; |
| 1091 | 1105 | |
| 1092 | 1106 | foreach ($patterns as $pattern) { |
| 1093 | 1107 | if (preg_match($pattern, $url, $matches)) { |
| @@ -1219,13 +1233,19 @@ | ||
| 1219 | 1233 | */ |
| 1220 | 1234 | private function getSimilarTrips(object $trip): array |
| 1221 | 1235 | { |
| 1222 | 1236 | $trip_id = (int) $trip->id; |
| 1237 | + | |
| 1238 | + // Hour-based day tours (3.0.14+ column) — guarded so an install whose | |
| 1239 | + // upgrade ALTER has not run yet keeps rendering similar trips. | |
| 1240 | + $duration_hours_col = (new \Yatra\Repositories\TripRepository())->hasTripColumn('duration_hours') | |
| 1241 | + ? ', duration_hours' | |
| 1242 | + : ''; | |
| 1223 | 1243 | |
| 1224 | 1244 | // Get similar trips based on category or difficulty |
| 1225 | 1245 | $similar = $this->wpdb->get_results( |
| 1226 | 1246 | $this->wpdb->prepare( |
| 1227 | - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights, | |
| 1247 | + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col}, | |
| 1228 | 1248 | original_price, sale_price, difficulty_level, |
| 1229 | 1249 | short_description |
| 1230 | 1250 | FROM {$this->table_trips} t |
| 1231 | 1251 | WHERE t.id != %d |
| @@ -1256,9 +1276,9 @@ | ||
| 1256 | 1276 | // Fallback: Get any published trips if no similar found |
| 1257 | 1277 | if (empty($similar)) { |
| 1258 | 1278 | $similar = $this->wpdb->get_results( |
| 1259 | 1279 | $this->wpdb->prepare( |
| 1260 | - "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights, | |
| 1280 | + "SELECT id, title, slug, featured_image AS featured_image_id, '' AS featured_image_url, duration_days, duration_nights{$duration_hours_col}, | |
| 1261 | 1281 | original_price, sale_price, difficulty_level, |
| 1262 | 1282 | short_description |
| 1263 | 1283 | FROM {$this->table_trips} |
| 1264 | 1284 | WHERE id != %d |
| @@ -1511,13 +1531,20 @@ | ||
| 1511 | 1531 | 'included' => !empty($entry->included_items) ? json_decode($entry->included_items, true) : [], |
| 1512 | 1532 | 'excluded' => !empty($entry->excluded_items) ? json_decode($entry->excluded_items, true) : [], |
| 1513 | 1533 | 'gallery' => !empty($entry->gallery) ? $this->decodeGallery($entry->gallery) : [], |
| 1514 | 1534 | 'video_url' => $entry->video_url ?: '', |
| 1535 | + // The admin "Notes / Instructions" textarea ("Additional notes | |
| 1536 | + // or special instructions for this activity") was stored but | |
| 1537 | + // never reached the public template — the array key was | |
| 1538 | + // simply absent. Without this, operators saw their notes | |
| 1539 | + // discarded silently on the live trip page. | |
| 1540 | + 'notes' => (string) ($entry->notes ?? ''), | |
| 1515 | 1541 | ]; |
| 1516 | 1542 | } |
| 1517 | 1543 | |
| 1518 | 1544 | $itinerary[] = [ |
| 1519 | 1545 | 'day' => (int) $day->day_number, |
| 1546 | + /* translators: %d: itinerary day number. */ | |
| 1520 | 1547 | 'day_title' => $day->title ?: sprintf(__('Day %d', 'yatra'), $day->day_number), |
| 1521 | 1548 | 'day_description' => $day->description ?: '', |
| 1522 | 1549 | 'entries' => $formatted_entries, |
| 1523 | 1550 | ]; |
| @@ -1812,12 +1839,15 @@ | ||
| 1812 | 1839 | $is_per_group = ($pricing_mode === 'per_group'); |
| 1813 | 1840 | $pricing_label = ''; |
| 1814 | 1841 | if ($is_per_group) { |
| 1815 | 1842 | if (!empty($price_type->min_pax) && !empty($price_type->max_pax)) { |
| 1816 | - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $price_type->min_pax, $price_type->max_pax); | |
| 1843 | + /* translators: 1: minimum pax for the group price, 2: maximum pax. */ | |
| 1844 | + $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $price_type->min_pax, $price_type->max_pax); | |
| 1817 | 1845 | } elseif (!empty($price_type->max_pax)) { |
| 1846 | + /* translators: %d: maximum pax for the group price. */ | |
| 1818 | 1847 | $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $price_type->max_pax); |
| 1819 | 1848 | } elseif (!empty($price_type->min_pax)) { |
| 1849 | + /* translators: %d: minimum pax for the group price. */ | |
| 1820 | 1850 | $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $price_type->min_pax); |
| 1821 | 1851 | } else { |
| 1822 | 1852 | $pricing_label = __('per group', 'yatra'); |
| 1823 | 1853 | } |
| @@ -1846,12 +1876,15 @@ | ||
| 1846 | 1876 | $age_min = $price_type->age_min ?? null; |
| 1847 | 1877 | $age_max = $price_type->age_max ?? null; |
| 1848 | 1878 | if ($age_min !== null || $age_max !== null) { |
| 1849 | 1879 | if ($age_min !== null && $age_max !== null) { |
| 1850 | - $age_info = sprintf(__('(Age %d-%d)', 'yatra'), $age_min, $age_max); | |
| 1880 | + /* translators: 1: minimum age, 2: maximum age. */ | |
| 1881 | + $age_info = sprintf(__('(Age %1$d-%2$d)', 'yatra'), $age_min, $age_max); | |
| 1851 | 1882 | } elseif ($age_min !== null) { |
| 1883 | + /* translators: %d: minimum age. */ | |
| 1852 | 1884 | $age_info = sprintf(__('(Age %d+)', 'yatra'), $age_min); |
| 1853 | 1885 | } else { |
| 1886 | + /* translators: %d: maximum age. */ | |
| 1854 | 1887 | $age_info = sprintf(__('(Up to age %d)', 'yatra'), $age_max); |
| 1855 | 1888 | } |
| 1856 | 1889 | } |
| 1857 | 1890 | |
| @@ -1864,8 +1897,16 @@ | ||
| 1864 | 1897 | |
| 1865 | 1898 | $input_id = 'traveler_' . $price_type->category_id; |
| 1866 | 1899 | $max_travelers = is_object($trip) && method_exists($trip, 'getMaxTravelers') ? $trip->getMaxTravelers() : ($trip->max_travelers ?? 20); |
| 1867 | 1900 | $pt_max_qty = (int) ($price_type->max_quantity ?: $max_travelers); |
| 1901 | + // A per-group category in "block" overflow mode caps the party at | |
| 1902 | + // the max group size. In "per_block" mode the party may exceed it | |
| 1903 | + // (it just buys additional group blocks), so we keep the trip's | |
| 1904 | + // normal cap there. | |
| 1905 | + if ($is_per_group && !empty($price_type->max_pax) | |
| 1906 | + && (($price_type->group_overflow ?? 'block') !== 'per_block')) { | |
| 1907 | + $pt_max_qty = (int) $price_type->max_pax; | |
| 1908 | + } | |
| 1868 | 1909 | $pt_value = ($index === $default_index) ? 1 : 0; |
| 1869 | 1910 | |
| 1870 | 1911 | $traveler_rows[] = [ |
| 1871 | 1912 | 'label' => $price_type->category_label ?: __('Traveler', 'yatra'), |
| @@ -1874,17 +1915,21 @@ | ||
| 1874 | 1915 | 'row_attrs' => [ |
| 1875 | 1916 | 'data-category-id' => $price_type->category_id, |
| 1876 | 1917 | 'data-price' => $price_type->effective_price, |
| 1877 | 1918 | 'data-pricing-mode' => $pricing_mode, |
| 1919 | + 'data-group-overflow' => $price_type->group_overflow ?? 'block', | |
| 1920 | + 'data-max-pax' => $price_type->max_pax ?? '', | |
| 1878 | 1921 | ], |
| 1879 | 1922 | 'minus_disabled' => ($index !== $default_index), |
| 1880 | 1923 | 'plus_disabled' => false, |
| 1881 | 1924 | 'minus_attrs' => [ |
| 1882 | 1925 | 'data-target' => $input_id, |
| 1926 | + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */ | |
| 1883 | 1927 | 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $price_type->category_label), |
| 1884 | 1928 | ], |
| 1885 | 1929 | 'plus_attrs' => [ |
| 1886 | 1930 | 'data-target' => $input_id, |
| 1931 | + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */ | |
| 1887 | 1932 | 'aria-label' => sprintf(__('Increase %s', 'yatra'), $price_type->category_label), |
| 1888 | 1933 | ], |
| 1889 | 1934 | 'input_attrs' => [ |
| 1890 | 1935 | 'id' => $input_id, |
| @@ -1895,8 +1940,10 @@ | ||
| 1895 | 1940 | 'data-category' => $price_type->category_id, |
| 1896 | 1941 | 'data-category-label' => $price_type->category_label, |
| 1897 | 1942 | 'data-price' => $price_type->effective_price, |
| 1898 | 1943 | 'data-pricing-mode' => $pricing_mode, |
| 1944 | + 'data-group-overflow' => $price_type->group_overflow ?? 'block', | |
| 1945 | + 'data-max-pax' => $price_type->max_pax ?? '', | |
| 1899 | 1946 | ], |
| 1900 | 1947 | ]; |
| 1901 | 1948 | } |
| 1902 | 1949 | |
| @@ -2029,9 +2076,10 @@ | ||
| 2029 | 2076 | foreach ($normalized_price_types as $pt_index => $pt) { |
| 2030 | 2077 | $pt_min = isset($pt->age_min) ? (int) $pt->age_min : 0; |
| 2031 | 2078 | $pt_max = isset($pt->age_max) ? (int) $pt->age_max : 99; |
| 2032 | 2079 | $pt_label = $pt->category_label ?? $pt->label ?? __('Traveler', 'yatra'); |
| 2033 | - $pt_age_text = ($pt_min > 0 || $pt_max < 99) ? sprintf(__('(Age %d-%d)', 'yatra'), $pt_min, $pt_max) : ''; | |
| 2080 | + /* translators: 1: minimum age, 2: maximum age. */ | |
| 2081 | + $pt_age_text = ($pt_min > 0 || $pt_max < 99) ? sprintf(__('(Age %1$d-%2$d)', 'yatra'), $pt_min, $pt_max) : ''; | |
| 2034 | 2082 | |
| 2035 | 2083 | // Use initial traveler count if provided, otherwise use default |
| 2036 | 2084 | $pt_category_id = $pt->category_id ?? $pt_index; |
| 2037 | 2085 | $pt_default = isset($initial_travelers[$pt_category_id]) |
| @@ -2070,17 +2118,26 @@ | ||
| 2070 | 2118 | $pt_min_qty = 0; |
| 2071 | 2119 | $pt_max_qty = (int) min($seats_available, $max_travelers); |
| 2072 | 2120 | $pt_pricing_mode = $pt->pricing_mode ?? 'per_person'; |
| 2073 | 2121 | $pt_is_per_group = ($pt_pricing_mode === 'per_group'); |
| 2074 | - | |
| 2122 | + // Cap a per-group "block" category at its max group size (still | |
| 2123 | + // bounded by seats). "per_block" mode may exceed it, so skip. | |
| 2124 | + if ($pt_is_per_group && !empty($pt->max_pax) | |
| 2125 | + && (($pt->group_overflow ?? 'block') !== 'per_block')) { | |
| 2126 | + $pt_max_qty = (int) min($pt_max_qty, (int) $pt->max_pax); | |
| 2127 | + } | |
| 2128 | + | |
| 2075 | 2129 | // Build pricing label |
| 2076 | 2130 | $pricing_label = ''; |
| 2077 | 2131 | if ($pt_is_per_group) { |
| 2078 | 2132 | if (!empty($pt->min_pax) && !empty($pt->max_pax)) { |
| 2079 | - $pricing_label = sprintf(__('per group (%d-%d pax)', 'yatra'), $pt->min_pax, $pt->max_pax); | |
| 2133 | + /* translators: 1: minimum pax for the group price, 2: maximum pax. */ | |
| 2134 | + $pricing_label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $pt->min_pax, $pt->max_pax); | |
| 2080 | 2135 | } elseif (!empty($pt->max_pax)) { |
| 2136 | + /* translators: %d: maximum pax for the group price. */ | |
| 2081 | 2137 | $pricing_label = sprintf(__('per group (up to %d pax)', 'yatra'), $pt->max_pax); |
| 2082 | 2138 | } elseif (!empty($pt->min_pax)) { |
| 2139 | + /* translators: %d: minimum pax for the group price. */ | |
| 2083 | 2140 | $pricing_label = sprintf(__('per group (%d+ pax)', 'yatra'), $pt->min_pax); |
| 2084 | 2141 | } else { |
| 2085 | 2142 | $pricing_label = __('per group', 'yatra'); |
| 2086 | 2143 | } |
| @@ -2100,17 +2157,21 @@ | ||
| 2100 | 2157 | 'row_attrs' => [ |
| 2101 | 2158 | 'data-category-id' => $pt_category_id, |
| 2102 | 2159 | 'data-price' => $pt_price, |
| 2103 | 2160 | 'data-pricing-mode' => $pt_pricing_mode, |
| 2161 | + 'data-group-overflow' => $pt->group_overflow ?? 'block', | |
| 2162 | + 'data-max-pax' => $pt->max_pax ?? '', | |
| 2104 | 2163 | ], |
| 2105 | 2164 | 'minus_disabled' => ($pt_default <= 0), |
| 2106 | 2165 | 'plus_disabled' => false, |
| 2107 | 2166 | 'minus_attrs' => [ |
| 2108 | 2167 | 'data-target' => 'traveler_' . $pt_category_id . '_' . $item_id, |
| 2168 | + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */ | |
| 2109 | 2169 | 'aria-label' => sprintf(__('Decrease %s', 'yatra'), $pt_label), |
| 2110 | 2170 | ], |
| 2111 | 2171 | 'plus_attrs' => [ |
| 2112 | 2172 | 'data-target' => 'traveler_' . $pt_category_id . '_' . $item_id, |
| 2173 | + /* translators: %s: traveler category label (e.g. "Adult", "Child"). */ | |
| 2113 | 2174 | 'aria-label' => sprintf(__('Increase %s', 'yatra'), $pt_label), |
| 2114 | 2175 | ], |
| 2115 | 2176 | 'input_attrs' => [ |
| 2116 | 2177 | 'data-item' => $item_id, |
| @@ -2116,8 +2177,10 @@ | ||
| 2116 | 2177 | 'data-item' => $item_id, |
| 2117 | 2178 | 'data-category' => $pt_category_id, |
| 2118 | 2179 | 'data-price' => $pt_price, |
| 2119 | 2180 | 'data-pricing-mode' => $pt_pricing_mode, |
| 2181 | + 'data-group-overflow' => $pt->group_overflow ?? 'block', | |
| 2182 | + 'data-max-pax' => $pt->max_pax ?? '', | |
| 2120 | 2183 | 'value' => $pt_default, |
| 2121 | 2184 | 'min' => $pt_min_qty, |
| 2122 | 2185 | 'max' => $pt_max_qty, |
| 2123 | 2186 | ], |