PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Shortcodes / DestinationShortcode.php

DestinationShortcode.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.8, at app/Shortcodes/DestinationShortcode.php

516 lines 20.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Shortcodes;
6
7 /**
8 * Destination Shortcode
9 *
10 * Displays destination showcase with associated trips using trip-listing-card.php template
11 */
12 class DestinationShortcode extends BaseShortcode
13 {
14 public function __construct()
15 {
16 parent::__construct('yatra_destination', [
17 'order' => 'desc',
18 'per_page' => '10',
19 'columns' => '3',
20 'show_trip_count' => 'yes',
21 'show_description' => 'yes',
22 'show_image' => 'yes',
23 'show_pagination' => 'yes', // Default to show pagination like trip shortcode
24 'destination' => '', // Specific destination slug(s), comma separated
25 'hide_empty' => 'yes',
26 'featured_only' => 'no',
27 'title' => 'Destination Showcase'
28 ]);
29 }
30
31 /**
32 * Render the destination shortcode content
33 */
34 protected function renderContent(array $atts): string
35 {
36 $atts = shortcode_atts($this->default_attributes, $atts, $this->tag);
37
38 // Extract per_page from attributes (only per_page parameter)
39 $per_page = 10; // default
40 if (!empty($atts['per_page']) && is_numeric($atts['per_page'])) {
41 $per_page = (int) $atts['per_page'];
42 }
43 $atts['per_page'] = $per_page;
44
45 // Get destinations using Yatra's service
46 $destinations_data = $this->getDestinations($atts);
47
48 // Prepare data for template
49 $data = [
50 'destinations' => $destinations_data['destinations'] ?? [],
51 'atts' => $atts,
52 'current_page' => $destinations_data['current_page'] ?? 1,
53 'max_pages' => $destinations_data['max_pages'] ?? 1,
54 'total_found' => $destinations_data['total_found'] ?? 0,
55 'per_page' => $per_page
56 ];
57
58 // Enqueue shortcode-specific CSS
59 wp_enqueue_style(
60 'yatra-destination-shortcode',
61 YATRA_PLUGIN_URL . 'assets/css/shortcodes/destination-shortcode.css',
62 [],
63 YATRA_VERSION
64 );
65
66 // Enqueue shortcode-specific JavaScript
67 wp_enqueue_script(
68 'yatra-destination-shortcode',
69 YATRA_PLUGIN_URL . 'assets/js/destination-shortcode.js',
70 ['jquery'],
71 YATRA_VERSION,
72 true
73 );
74
75 // Pass data to JavaScript
76 wp_localize_script('yatra-destination-shortcode', 'yatraDestinationShortcode', [
77 'ajaxurl' => admin_url('admin-ajax.php'),
78 'nonce' => wp_create_nonce('yatra_destination_shortcode_nonce')
79 ]);
80
81 return $this->loadTemplate('shortcodes/destination.php', $data);
82 }
83
84 /**
85 * Get destinations using Yatra's service
86 */
87 public function getDestinations(array $atts): array
88 {
89 try {
90 $destinationService = new \Yatra\Services\DestinationService();
91
92 // Get current page from query string or attributes (for AJAX)
93 $current_page = isset($atts['current_page']) ? (int) $atts['current_page'] : (isset($_GET['destination_page']) ? (int) $_GET['destination_page'] : 1);
94 // Use per_page parameter only
95 $per_page = 10; // Default fallback
96 if (!empty($atts['per_page'])) {
97 $per_page = (int) $atts['per_page'];
98 if (defined('WP_DEBUG') && WP_DEBUG) {
99 error_log('Yatra DestinationShortcode: Using per_page = ' . $per_page);
100 }
101 } else {
102 if (defined('WP_DEBUG') && WP_DEBUG) {
103 error_log('Yatra DestinationShortcode: Using default per_page = ' . $per_page);
104 }
105 }
106
107 // Validate per_page to prevent issues
108 $per_page = max(1, min($per_page, 100)); // Between 1 and 100 items
109 $offset = ($current_page - 1) * $per_page;
110
111 // Start with very basic arguments to ensure we get destinations
112 $args = [
113 'limit' => $per_page,
114 'offset' => $offset,
115 'order_by' => 'name',
116 'order' => $atts['order'] === 'asc' ? 'ASC' : 'DESC'
117 ];
118
119 // Filter by specific destinations if provided
120 if (!empty($atts['destination'])) {
121 $args['where']['slug'] = explode(',', $atts['destination']);
122 }
123
124 // Get total count for pagination
125 $count_args = $args;
126 unset($count_args['limit']);
127 unset($count_args['offset']);
128 $total_destinations = $destinationService->count($count_args);
129
130 // Try using the base repository method to bypass status filtering
131 $result = $destinationService->getAll($args);
132
133 // Debug: Log the results with enhanced pagination info
134 if (defined('WP_DEBUG') && WP_DEBUG) {
135 error_log('=== YATRA DESTINATION PAGINATION DEBUG ===');
136 error_log('Yatra DestinationShortcode Args: ' . print_r($args, true));
137 error_log('Yatra DestinationShortcode Results count: ' . count($result));
138 error_log('Yatra DestinationShortcode Total destinations: ' . $total_destinations);
139 error_log('Yatra DestinationShortcode Pagination Info:');
140 error_log(' - Current Page: ' . $current_page);
141 error_log(' - Per Page: ' . $per_page);
142 error_log(' - Offset: ' . $offset);
143 error_log(' - Max Pages: ' . ($per_page > 0 ? ceil($total_destinations / $per_page) : 1));
144 error_log(' - Expected items on this page: ' . min($per_page, max(0, $total_destinations - $offset)));
145 error_log(' - SQL LIMIT clause should be: LIMIT ' . $offset . ', ' . $per_page);
146
147 // Log each destination being returned
148 if (!empty($result)) {
149 error_log('Destinations returned on page ' . $current_page . ':');
150 foreach ($result as $index => $destination) {
151 error_log(' ' . ($index + 1) . '. ' . ($destination->name ?? 'NO NAME') . ' (ID: ' . $destination->id . ')');
152 }
153 }
154 error_log('=== END PAGINATION DEBUG ===');
155 }
156
157 $destinations = [];
158
159 foreach ($result as $destinationData) {
160 // Get real trip data for this destination using classification tables
161 global $wpdb;
162
163 $tripClassificationsTable = \Yatra\Database\Tables\TripClassificationsTable::getTableName();
164 $tripsTable = \Yatra\Database\Tables\TripsTable::getTableName();
165
166 // Get trip IDs for this destination
167 $trip_ids = $wpdb->get_col($wpdb->prepare(
168 "SELECT tc.trip_id
169 FROM {$tripClassificationsTable} tc
170 INNER JOIN {$tripsTable} t ON tc.trip_id = t.id
171 WHERE tc.classification_id = %d
172 AND tc.classification_type = 'destination'
173 AND t.status = 'publish'",
174 $destinationData->id
175 ));
176
177 $trip_count = count($trip_ids);
178
179 // Get actual trips
180 $trips = [];
181 if (!empty($trip_ids)) {
182 $placeholders = implode(',', array_fill(0, count($trip_ids), '%d'));
183 $trips = $wpdb->get_results($wpdb->prepare(
184 "SELECT * FROM {$tripsTable}
185 WHERE id IN ({$placeholders})
186 AND status = 'publish'
187 ORDER BY created_at DESC
188 LIMIT 6",
189 ...$trip_ids
190 ));
191 }
192
193 // Calculate pricing from real trips
194 $min_price = null;
195 $max_price = null;
196 $durations = [];
197 $group_sizes = [];
198 $best_seasons = [];
199
200 // Calculate rating from reviews table directly
201 $total_rating_sum = 0;
202 $total_review_count = 0;
203
204 if (!empty($trip_ids)) {
205 $reviewsTable = \Yatra\Database\Tables\ReviewsTable::getTableName();
206 $placeholders = implode(',', array_fill(0, count($trip_ids), '%d'));
207
208 $reviews = $wpdb->get_results($wpdb->prepare(
209 "SELECT rating, COUNT(*) as review_count
210 FROM {$reviewsTable}
211 WHERE trip_id IN ({$placeholders})
212 AND status = 'approved'",
213 ...$trip_ids
214 ));
215
216 if (defined('WP_DEBUG') && WP_DEBUG) {
217 error_log('DESTINATION REVIEWS QUERY: ' . print_r($reviews, true));
218 }
219
220 foreach ($reviews as $review) {
221 $total_rating_sum += $review->rating * $review->review_count;
222 $total_review_count += $review->review_count;
223
224 if (defined('WP_DEBUG') && WP_DEBUG) {
225 error_log('DESTINATION RATING CALCULATION: Added ' . $review->rating . ' * ' . $review->review_count . ' = ' . ($review->rating * $review->review_count));
226 }
227 }
228 }
229
230 // Calculate average rating only for trips that actually have reviews
231 // Trips with no reviews are excluded from the average (not treated as 0 rating)
232 $avg_rating = $total_review_count > 0 ? $total_rating_sum / $total_review_count : 0;
233
234 foreach ($trips as $trip) {
235 // Debug: Log all trip data to see what fields exist
236 if (defined('WP_DEBUG') && WP_DEBUG) {
237 error_log('DESTINATION TRIP DATA: ' . print_r([
238 'trip_id' => $trip->id ?? 'NO ID',
239 'trip_title' => $trip->title ?? 'NO TITLE',
240 'original_price' => $trip->original_price ?? 'NO ORIGINAL_PRICE',
241 'discounted_price' => $trip->discounted_price ?? 'NO DISCOUNTED_PRICE',
242 'sale_price' => $trip->sale_price ?? 'NO SALE_PRICE',
243 'base_price' => $trip->base_price ?? 'NO BASE_PRICE',
244 'price' => $trip->price ?? 'NO PRICE',
245 'all_fields' => array_keys(get_object_vars($trip))
246 ], true));
247 }
248
249 // Get pricing via centralized TripPricingService
250 $effective = \Yatra\Services\TripPricingService::getEffectivePrice($trip);
251 if ($effective > 0) {
252 if ($min_price === null || $effective < $min_price) {
253 $min_price = $effective;
254 }
255 if ($max_price === null || $effective > $max_price) {
256 $max_price = $effective;
257 }
258 }
259
260 // Get duration
261 if (!empty($trip->duration)) {
262 $durations[] = $trip->duration;
263 }
264
265 // Get group size
266 if (!empty($trip->max_group_size)) {
267 $group_sizes[] = $trip->max_group_size;
268 }
269
270 // Get best season
271 if (!empty($trip->best_season)) {
272 $best_seasons[] = $trip->best_season;
273 }
274 }
275
276 // Calculate averages
277 $final_avg_rating = $avg_rating; // Already calculated correctly above
278 $avg_duration = !empty($durations) ? array_sum($durations) / count($durations) : 0;
279 $avg_group_size = !empty($group_sizes) ? round(array_sum($group_sizes) / count($group_sizes)) : 0;
280 $best_season = !empty($best_seasons) ? $this->getMostCommonSeason($best_seasons) : 'Summer';
281
282 if (defined('WP_DEBUG') && WP_DEBUG) {
283 error_log('FINAL DESTINATION RATING CALCULATION:');
284 error_log('Destination: ' . ($destinationData->name ?? 'NO NAME'));
285 error_log('Total rating sum: ' . $total_rating_sum);
286 error_log('Total review count: ' . $total_review_count);
287 error_log('Number of trips: ' . $trip_count);
288 error_log('Logic: Only trips with reviews are included in average');
289 error_log('Final avg_rating: ' . $final_avg_rating);
290 }
291
292 $destinations[] = [
293 'term' => $destinationData,
294 'trips' => $trips,
295 'trip_count' => $trip_count,
296 'description' => $destinationData->description ?? '',
297 'image' => $this->getDestinationImage($destinationData, $trips),
298 'link' => $this->getDestinationLink($destinationData),
299 'country' => $destinationData->country ?? '',
300 'region' => $destinationData->region ?? '',
301 'min_price' => $min_price,
302 'max_price' => $max_price,
303 'avg_rating' => $final_avg_rating,
304 'rating_count' => $total_review_count,
305 'avg_duration' => $avg_duration,
306 'avg_group_size' => $avg_group_size,
307 'best_season' => $best_season
308 ];
309 }
310
311 // Filter out empty destinations if requested
312 if ($atts['hide_empty'] === 'yes') {
313 $destinations = array_filter($destinations, function($destination) {
314 return !empty($destination['term']->name) && !empty($destination['term']->slug);
315 });
316 }
317
318 // Filter to show only featured destinations if requested
319 if ($atts['featured_only'] === 'yes') {
320 $destinations = array_filter($destinations, function($destination) {
321 // Check if destination is marked as featured
322 return isset($destination['term']->featured) && $destination['term']->featured == 1;
323 });
324 }
325
326 // Calculate pagination data
327 $max_pages = $per_page > 0 ? ceil($total_destinations / $per_page) : 1;
328
329 return [
330 'destinations' => $destinations,
331 'current_page' => $current_page,
332 'max_pages' => $max_pages,
333 'total_found' => $total_destinations,
334 'per_page' => $per_page
335 ];
336
337 } catch (\Exception $e) {
338 if (defined('WP_DEBUG') && WP_DEBUG) {
339 error_log('Yatra DestinationShortcode Error: ' . $e->getMessage());
340 }
341 return [];
342 }
343 }
344
345 /**
346 * Get the most common season from an array of seasons
347 */
348 private function getMostCommonSeason(array $seasons): string
349 {
350 if (empty($seasons)) {
351 return 'Summer';
352 }
353
354 $counts = array_count_values($seasons);
355 arsort($counts);
356 return array_key_first($counts);
357 }
358
359
360 /**
361 * Resolve image URL from destination `icon` (same shape as admin: type image|icon, value = attachment ID or URL).
362 */
363 private function getImageUrlFromDestinationIcon($icon): string
364 {
365 if ($icon === null || $icon === '') {
366 return '';
367 }
368
369 if (is_string($icon)) {
370 $decoded = json_decode($icon, true);
371 if (is_array($decoded)) {
372 $icon = $decoded;
373 } else {
374 $icon = maybe_unserialize($icon);
375 }
376 }
377
378 if (!is_array($icon)) {
379 return '';
380 }
381
382 $type = $icon['type'] ?? $icon[0] ?? '';
383 $value = $icon['value'] ?? $icon[1] ?? '';
384
385 if ($type !== 'image' || $value === '' || $value === null) {
386 return '';
387 }
388
389 if (is_numeric($value)) {
390 $url = wp_get_attachment_image_url((int) $value, 'large');
391
392 return $url ?: '';
393 }
394
395 if (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
396 return $value;
397 }
398
399 return '';
400 }
401
402 /**
403 * Decode metadata column to array (JSON or PHP serialized).
404 *
405 * @return array<string, mixed>
406 */
407 private function decodeDestinationMetadata($raw): array
408 {
409 if ($raw === null || $raw === '') {
410 return [];
411 }
412
413 if (is_array($raw)) {
414 return $raw;
415 }
416
417 if (!is_string($raw)) {
418 return [];
419 }
420
421 $decoded = json_decode($raw, true);
422 if (is_array($decoded)) {
423 return $decoded;
424 }
425
426 $unserialized = maybe_unserialize($raw);
427
428 return is_array($unserialized) ? $unserialized : [];
429 }
430
431 /**
432 * Get destination image URL for shortcode cards.
433 *
434 * @param object $destination Classification row from DB
435 * @param array<int, object> $trips Associated trips (newest first), used for featured_image fallback
436 */
437 private function getDestinationImage($destination, array $trips = []): string
438 {
439 // Check for destination image in metadata
440 if (isset($destination->image) && !empty($destination->image)) {
441 return $destination->image;
442 }
443
444 // Check for destination banner in metadata
445 if (isset($destination->banner) && !empty($destination->banner)) {
446 return $destination->banner;
447 }
448
449 // Check for destination thumbnail/featured image
450 if (isset($destination->thumbnail) && !empty($destination->thumbnail)) {
451 return is_numeric($destination->thumbnail) ? (string) wp_get_attachment_url((int) $destination->thumbnail) : $destination->thumbnail;
452 }
453
454 // Check for destination cover image
455 if (isset($destination->cover_image) && !empty($destination->cover_image)) {
456 return $destination->cover_image;
457 }
458
459 // Check for destination hero image
460 if (isset($destination->hero_image) && !empty($destination->hero_image)) {
461 return $destination->hero_image;
462 }
463
464 $fromIcon = $this->getImageUrlFromDestinationIcon($destination->icon ?? null);
465 if ($fromIcon !== '') {
466 return $fromIcon;
467 }
468
469 // Check for metadata with image (JSON or serialized)
470 if (isset($destination->metadata) && $destination->metadata !== '') {
471 $metadata = $this->decodeDestinationMetadata($destination->metadata);
472 if ($metadata !== []) {
473 $image_fields = ['image', 'thumbnail', 'banner', 'featured_image', 'cover_image', 'hero_image', 'image_id'];
474 foreach ($image_fields as $field) {
475 if (!empty($metadata[$field])) {
476 $val = $metadata[$field];
477
478 return is_numeric($val) ? (string) wp_get_attachment_url((int) $val) : (string) $val;
479 }
480 }
481 }
482 }
483
484 foreach ($trips as $trip) {
485 if (!empty($trip->featured_image) && is_numeric($trip->featured_image)) {
486 $url = wp_get_attachment_image_url((int) $trip->featured_image, 'large');
487 if ($url !== false && $url !== '') {
488 return $url;
489 }
490 }
491 }
492
493 // Fallback to placeholder
494 $fallback_url = YATRA_PLUGIN_URL . 'assets/images/placeholder.png';
495
496 if (defined('WP_DEBUG') && WP_DEBUG) {
497 error_log('Yatra DestinationShortcode Using fallback image: ' . $fallback_url);
498 }
499
500 return $fallback_url;
501 }
502
503 /**
504 * Get destination link
505 */
506 private function getDestinationLink($destination): string
507 {
508 // Try to get permalink from destination service or construct it
509 if (isset($destination->slug)) {
510 return home_url("/destination/{$destination->slug}/");
511 }
512
513 return '#'; // Fallback
514 }
515 }
516