| @@ -233,8 +233,17 @@ | ||
| 233 | 233 | public array $tags = []; |
| 234 | 234 | public array $included_items = []; |
| 235 | 235 | public array $excluded_items = []; |
| 236 | 236 | public array $gallery_images = []; |
| 237 | + // Media arrays computed in SingleTripController::prepareTrip(). These MUST be | |
| 238 | + // declared so Trip::fromStdClass() (which only copies declared properties via | |
| 239 | + // property_exists) carries them onto the model — otherwise the single-trip | |
| 240 | + // hero media switcher reads empty arrays and the Video / 360° / Documents | |
| 241 | + // buttons never render even when video_url / virtual_tour_url are set. | |
| 242 | + public array $videos = []; | |
| 243 | + public array $youtube_videos = []; | |
| 244 | + public array $virtual_tours = []; | |
| 245 | + public array $documents = []; | |
| 237 | 246 | public array $price_types = []; |
| 238 | 247 | public array $itinerary_days = []; |
| 239 | 248 | public array $faqs = []; |
| 240 | 249 | public array $frontend_tabs = []; |
| @@ -654,8 +663,21 @@ | ||
| 654 | 663 | ]; |
| 655 | 664 | } |
| 656 | 665 | |
| 657 | 666 | /** |
| 667 | + * Returns true when the trip is set to enquiry-only (booking calendar disabled). | |
| 668 | + * Stored as custom_fields['disable_booking'] = true. Requires Pro to be active. | |
| 669 | + */ | |
| 670 | + public function isBookingDisabled(): bool | |
| 671 | + { | |
| 672 | + if (!apply_filters('yatra_is_pro_active', false)) { | |
| 673 | + return false; | |
| 674 | + } | |
| 675 | + | |
| 676 | + return !empty($this->custom_fields['disable_booking']); | |
| 677 | + } | |
| 678 | + | |
| 679 | + /** | |
| 658 | 680 | * Parse JSON field from database |
| 659 | 681 | */ |
| 660 | 682 | private static function parseJsonField(?string $value, array $default = []): array |
| 661 | 683 | { |
| @@ -1004,80 +1026,127 @@ | ||
| 1004 | 1026 | ]; |
| 1005 | 1027 | } |
| 1006 | 1028 | |
| 1007 | 1029 | /** |
| 1008 | - * Get difficulty information | |
| 1030 | + * Resolve difficulty label + icon for display (shared by single trip, similar trips, etc.). | |
| 1031 | + * | |
| 1032 | + * @param string|null $difficultyLevel Raw trip.difficulty_level (usually classification row id). | |
| 1033 | + * @param string|null $difficultyName Optional joined name from list queries. | |
| 1034 | + * @param mixed|null $difficultyIcon Optional serialized icon payload from list queries. | |
| 1035 | + * @return array{level: string, icon: string, icon_picker: array<string, mixed>|null, has_difficulty: bool} | |
| 1009 | 1036 | */ |
| 1010 | - public function getDifficulty(): array | |
| 1011 | - { | |
| 1037 | + public static function resolveDifficultyDisplay( | |
| 1038 | + ?string $difficultyLevel, | |
| 1039 | + ?string $difficultyName = null, | |
| 1040 | + $difficultyIcon = null | |
| 1041 | + ): array { | |
| 1012 | 1042 | $difficulty = ''; |
| 1013 | 1043 | $difficulty_icon = ''; |
| 1014 | - | |
| 1015 | - // Always prioritize fetching from difficulty_levels table if we have a difficulty_level ID | |
| 1016 | - // Check if difficulty_level is set and is a valid positive integer (not 0, null, or empty) | |
| 1017 | - if (!empty($this->difficulty_level) && is_numeric($this->difficulty_level) && (int) $this->difficulty_level > 0) { | |
| 1044 | + /** @var array<string, mixed>|null $icon_picker */ | |
| 1045 | + $icon_picker = null; | |
| 1046 | + | |
| 1047 | + if (!empty($difficultyLevel) && is_numeric($difficultyLevel) && (int) $difficultyLevel > 0) { | |
| 1018 | 1048 | global $wpdb; |
| 1019 | - | |
| 1049 | + | |
| 1020 | 1050 | $difficulty_data = $wpdb->get_row($wpdb->prepare( |
| 1021 | - "SELECT * FROM " . ClassificationsTable::getTableName() . " WHERE id = %d AND type = %s", | |
| 1022 | - (int) $this->difficulty_level, | |
| 1051 | + 'SELECT * FROM ' . ClassificationsTable::getTableName() . ' WHERE id = %d AND type = %s', | |
| 1052 | + (int) $difficultyLevel, | |
| 1023 | 1053 | ClassificationTypes::DIFFICULTY |
| 1024 | 1054 | )); |
| 1025 | - | |
| 1055 | + | |
| 1026 | 1056 | if ($difficulty_data) { |
| 1027 | - $difficulty = $difficulty_data->name; | |
| 1057 | + $difficulty = (string) $difficulty_data->name; | |
| 1028 | 1058 | if (!empty($difficulty_data->icon)) { |
| 1029 | 1059 | $icon_data = maybe_unserialize($difficulty_data->icon); |
| 1030 | - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1031 | - $difficulty_icon = $icon_data['value']; | |
| 1060 | + if (is_array($icon_data) && isset($icon_data['type'])) { | |
| 1061 | + $icon_picker = $icon_data; | |
| 1062 | + if ($icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1063 | + $difficulty_icon = (string) $icon_data['value']; | |
| 1064 | + } | |
| 1032 | 1065 | } elseif (is_string($difficulty_data->icon)) { |
| 1033 | 1066 | $difficulty_icon = $difficulty_data->icon; |
| 1067 | + $icon_picker = [ | |
| 1068 | + 'type' => 'icon', | |
| 1069 | + 'value' => $difficulty_icon, | |
| 1070 | + 'provider' => 'yatra', | |
| 1071 | + ]; | |
| 1034 | 1072 | } |
| 1035 | 1073 | } |
| 1036 | 1074 | } |
| 1037 | 1075 | } |
| 1038 | - | |
| 1039 | - // Fallback: Use pre-populated difficulty_name from main query (AppServiceProvider) | |
| 1040 | - if (empty($difficulty) && !empty($this->difficulty_name)) { | |
| 1041 | - $difficulty = $this->difficulty_name; | |
| 1076 | + | |
| 1077 | + if ($difficulty === '' && !empty($difficultyName)) { | |
| 1078 | + $difficulty = $difficultyName; | |
| 1042 | 1079 | } |
| 1043 | - | |
| 1044 | - // Fallback: Use pre-populated difficulty_icon from main query (AppServiceProvider) | |
| 1045 | - if (empty($difficulty_icon) && !empty($this->difficulty_icon)) { | |
| 1046 | - $icon_data = maybe_unserialize($this->difficulty_icon); | |
| 1047 | - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1048 | - $difficulty_icon = $icon_data['value']; | |
| 1049 | - } elseif (is_string($this->difficulty_icon)) { | |
| 1050 | - $difficulty_icon = $this->difficulty_icon; | |
| 1080 | + | |
| 1081 | + // Legacy / non-id values stored in difficulty_level (e.g. slug text). | |
| 1082 | + if ($difficulty === '' && $difficultyLevel !== null && $difficultyLevel !== '' && !is_numeric($difficultyLevel)) { | |
| 1083 | + $difficulty = ucfirst($difficultyLevel); | |
| 1084 | + } | |
| 1085 | + | |
| 1086 | + if ($icon_picker === null && $difficultyIcon !== null && $difficultyIcon !== '') { | |
| 1087 | + $icon_data = is_string($difficultyIcon) ? maybe_unserialize($difficultyIcon) : $difficultyIcon; | |
| 1088 | + if (is_array($icon_data) && isset($icon_data['type'])) { | |
| 1089 | + $icon_picker = $icon_data; | |
| 1090 | + if ($icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1091 | + $difficulty_icon = (string) $icon_data['value']; | |
| 1092 | + } | |
| 1093 | + } elseif (is_string($difficultyIcon)) { | |
| 1094 | + $difficulty_icon = $difficultyIcon; | |
| 1095 | + $icon_picker = [ | |
| 1096 | + 'type' => 'icon', | |
| 1097 | + 'value' => $difficulty_icon, | |
| 1098 | + 'provider' => 'yatra', | |
| 1099 | + ]; | |
| 1051 | 1100 | } |
| 1052 | 1101 | } |
| 1053 | - | |
| 1054 | - // If we have difficulty but no icon, provide a default icon | |
| 1055 | - if (!empty($difficulty) && empty($difficulty_icon)) { | |
| 1102 | + | |
| 1103 | + if ($difficulty !== '' && $icon_picker === null && $difficulty_icon === '') { | |
| 1056 | 1104 | $difficulty_icon = 'mountain'; |
| 1105 | + $icon_picker = [ | |
| 1106 | + 'type' => 'icon', | |
| 1107 | + 'value' => 'mountain', | |
| 1108 | + 'provider' => 'yatra', | |
| 1109 | + ]; | |
| 1057 | 1110 | } |
| 1058 | - | |
| 1111 | + | |
| 1059 | 1112 | return [ |
| 1060 | 1113 | 'level' => $difficulty, |
| 1061 | 1114 | 'icon' => $difficulty_icon, |
| 1062 | - 'has_difficulty' => !empty($difficulty) | |
| 1115 | + 'icon_picker' => $icon_picker, | |
| 1116 | + 'has_difficulty' => $difficulty !== '', | |
| 1063 | 1117 | ]; |
| 1064 | 1118 | } |
| 1065 | 1119 | |
| 1066 | 1120 | /** |
| 1121 | + * Get difficulty information | |
| 1122 | + */ | |
| 1123 | + public function getDifficulty(): array | |
| 1124 | + { | |
| 1125 | + return self::resolveDifficultyDisplay( | |
| 1126 | + $this->difficulty_level, | |
| 1127 | + $this->difficulty_name, | |
| 1128 | + $this->difficulty_icon | |
| 1129 | + ); | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + /** | |
| 1067 | 1133 | * Get trip duration information |
| 1068 | 1134 | */ |
| 1069 | 1135 | public function getDuration(): array |
| 1070 | 1136 | { |
| 1071 | 1137 | $duration = ''; |
| 1072 | - if (!empty($this->duration_days)) { | |
| 1138 | + if ($this->isHoursBased()) { | |
| 1139 | + $duration = yatra_format_duration(0, null, $this->getDurationHours()); | |
| 1140 | + } elseif (!empty($this->duration_days)) { | |
| 1073 | 1141 | $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null); |
| 1074 | 1142 | } |
| 1075 | - | |
| 1143 | + | |
| 1076 | 1144 | return [ |
| 1077 | 1145 | 'formatted' => $duration, |
| 1078 | 1146 | 'days' => $this->duration_days ?? 0, |
| 1079 | 1147 | 'nights' => $this->duration_nights ?? 0, |
| 1148 | + 'hours' => $this->getDurationHours(), | |
| 1080 | 1149 | 'has_duration' => !empty($duration) |
| 1081 | 1150 | ]; |
| 1082 | 1151 | } |
| 1083 | 1152 | |
| @@ -1497,9 +1566,32 @@ | ||
| 1497 | 1566 | return $this->duration_nights ?? 0; |
| 1498 | 1567 | } |
| 1499 | 1568 | |
| 1500 | 1569 | /** |
| 1501 | - * Get difficulty level | |
| 1570 | + * Duration in hours for hour-based (single-day) tours. 0 when the tour is | |
| 1571 | + * day-based (the default) — see {@see self::isHoursBased()}. | |
| 1572 | + */ | |
| 1573 | + public function getDurationHours(): int | |
| 1574 | + { | |
| 1575 | + return (int) ($this->duration_hours ?? 0); | |
| 1576 | + } | |
| 1577 | + | |
| 1578 | + /** | |
| 1579 | + * Whether this tour's duration is expressed in hours rather than days. | |
| 1580 | + * | |
| 1581 | + * True only when a positive `duration_hours` is set — so every existing | |
| 1582 | + * (day-based) trip returns false and keeps its current behaviour. Used by | |
| 1583 | + * the front-end duration display and the Google Calendar module to build a | |
| 1584 | + * same-day timed event instead of a multi-day span. | |
| 1585 | + */ | |
| 1586 | + public function isHoursBased(): bool | |
| 1587 | + { | |
| 1588 | + return $this->getDurationHours() > 0; | |
| 1589 | + } | |
| 1590 | + | |
| 1591 | + /** | |
| 1592 | + * Raw difficulty classification ID on the trip row (not a human-readable label). | |
| 1593 | + * For display, use {@see self::getDifficulty()} which resolves the name from the classifications table. | |
| 1502 | 1594 | */ |
| 1503 | 1595 | public function getDifficultyLevel(): string |
| 1504 | 1596 | { |
| 1505 | 1597 | return $this->difficulty_level ?? ''; |