| @@ -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 = []; |
| @@ -1017,80 +1026,127 @@ | ||
| 1017 | 1026 | ]; |
| 1018 | 1027 | } |
| 1019 | 1028 | |
| 1020 | 1029 | /** |
| 1021 | - * 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} | |
| 1022 | 1036 | */ |
| 1023 | - public function getDifficulty(): array | |
| 1024 | - { | |
| 1037 | + public static function resolveDifficultyDisplay( | |
| 1038 | + ?string $difficultyLevel, | |
| 1039 | + ?string $difficultyName = null, | |
| 1040 | + $difficultyIcon = null | |
| 1041 | + ): array { | |
| 1025 | 1042 | $difficulty = ''; |
| 1026 | 1043 | $difficulty_icon = ''; |
| 1027 | - | |
| 1028 | - // Always prioritize fetching from difficulty_levels table if we have a difficulty_level ID | |
| 1029 | - // Check if difficulty_level is set and is a valid positive integer (not 0, null, or empty) | |
| 1030 | - 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) { | |
| 1031 | 1048 | global $wpdb; |
| 1032 | - | |
| 1049 | + | |
| 1033 | 1050 | $difficulty_data = $wpdb->get_row($wpdb->prepare( |
| 1034 | - "SELECT * FROM " . ClassificationsTable::getTableName() . " WHERE id = %d AND type = %s", | |
| 1035 | - (int) $this->difficulty_level, | |
| 1051 | + 'SELECT * FROM ' . ClassificationsTable::getTableName() . ' WHERE id = %d AND type = %s', | |
| 1052 | + (int) $difficultyLevel, | |
| 1036 | 1053 | ClassificationTypes::DIFFICULTY |
| 1037 | 1054 | )); |
| 1038 | - | |
| 1055 | + | |
| 1039 | 1056 | if ($difficulty_data) { |
| 1040 | - $difficulty = $difficulty_data->name; | |
| 1057 | + $difficulty = (string) $difficulty_data->name; | |
| 1041 | 1058 | if (!empty($difficulty_data->icon)) { |
| 1042 | 1059 | $icon_data = maybe_unserialize($difficulty_data->icon); |
| 1043 | - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1044 | - $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 | + } | |
| 1045 | 1065 | } elseif (is_string($difficulty_data->icon)) { |
| 1046 | 1066 | $difficulty_icon = $difficulty_data->icon; |
| 1067 | + $icon_picker = [ | |
| 1068 | + 'type' => 'icon', | |
| 1069 | + 'value' => $difficulty_icon, | |
| 1070 | + 'provider' => 'yatra', | |
| 1071 | + ]; | |
| 1047 | 1072 | } |
| 1048 | 1073 | } |
| 1049 | 1074 | } |
| 1050 | 1075 | } |
| 1051 | - | |
| 1052 | - // Fallback: Use pre-populated difficulty_name from main query (AppServiceProvider) | |
| 1053 | - if (empty($difficulty) && !empty($this->difficulty_name)) { | |
| 1054 | - $difficulty = $this->difficulty_name; | |
| 1076 | + | |
| 1077 | + if ($difficulty === '' && !empty($difficultyName)) { | |
| 1078 | + $difficulty = $difficultyName; | |
| 1055 | 1079 | } |
| 1056 | - | |
| 1057 | - // Fallback: Use pre-populated difficulty_icon from main query (AppServiceProvider) | |
| 1058 | - if (empty($difficulty_icon) && !empty($this->difficulty_icon)) { | |
| 1059 | - $icon_data = maybe_unserialize($this->difficulty_icon); | |
| 1060 | - if (is_array($icon_data) && isset($icon_data['type']) && $icon_data['type'] === 'icon' && !empty($icon_data['value'])) { | |
| 1061 | - $difficulty_icon = $icon_data['value']; | |
| 1062 | - } elseif (is_string($this->difficulty_icon)) { | |
| 1063 | - $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 | + ]; | |
| 1064 | 1100 | } |
| 1065 | 1101 | } |
| 1066 | - | |
| 1067 | - // If we have difficulty but no icon, provide a default icon | |
| 1068 | - if (!empty($difficulty) && empty($difficulty_icon)) { | |
| 1102 | + | |
| 1103 | + if ($difficulty !== '' && $icon_picker === null && $difficulty_icon === '') { | |
| 1069 | 1104 | $difficulty_icon = 'mountain'; |
| 1105 | + $icon_picker = [ | |
| 1106 | + 'type' => 'icon', | |
| 1107 | + 'value' => 'mountain', | |
| 1108 | + 'provider' => 'yatra', | |
| 1109 | + ]; | |
| 1070 | 1110 | } |
| 1071 | - | |
| 1111 | + | |
| 1072 | 1112 | return [ |
| 1073 | 1113 | 'level' => $difficulty, |
| 1074 | 1114 | 'icon' => $difficulty_icon, |
| 1075 | - 'has_difficulty' => !empty($difficulty) | |
| 1115 | + 'icon_picker' => $icon_picker, | |
| 1116 | + 'has_difficulty' => $difficulty !== '', | |
| 1076 | 1117 | ]; |
| 1077 | 1118 | } |
| 1078 | 1119 | |
| 1079 | 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 | + /** | |
| 1080 | 1133 | * Get trip duration information |
| 1081 | 1134 | */ |
| 1082 | 1135 | public function getDuration(): array |
| 1083 | 1136 | { |
| 1084 | 1137 | $duration = ''; |
| 1085 | - if (!empty($this->duration_days)) { | |
| 1138 | + if ($this->isHoursBased()) { | |
| 1139 | + $duration = yatra_format_duration(0, null, $this->getDurationHours()); | |
| 1140 | + } elseif (!empty($this->duration_days)) { | |
| 1086 | 1141 | $duration = yatra_format_duration($this->duration_days, $this->duration_nights ?? null); |
| 1087 | 1142 | } |
| 1088 | - | |
| 1143 | + | |
| 1089 | 1144 | return [ |
| 1090 | 1145 | 'formatted' => $duration, |
| 1091 | 1146 | 'days' => $this->duration_days ?? 0, |
| 1092 | 1147 | 'nights' => $this->duration_nights ?? 0, |
| 1148 | + 'hours' => $this->getDurationHours(), | |
| 1093 | 1149 | 'has_duration' => !empty($duration) |
| 1094 | 1150 | ]; |
| 1095 | 1151 | } |
| 1096 | 1152 | |
| @@ -1510,9 +1566,32 @@ | ||
| 1510 | 1566 | return $this->duration_nights ?? 0; |
| 1511 | 1567 | } |
| 1512 | 1568 | |
| 1513 | 1569 | /** |
| 1514 | - * 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. | |
| 1515 | 1594 | */ |
| 1516 | 1595 | public function getDifficultyLevel(): string |
| 1517 | 1596 | { |
| 1518 | 1597 | return $this->difficulty_level ?? ''; |