From 9c5a6747bcbac03512b6f270227eb00766257088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 14 Jul 2026 16:29:21 +0200 Subject: [PATCH] feat: consolidate event search teasers addresses #869e4n8py --- .../Classes/Service/FilterService.php | 51 +++++++----- .../Classes/Service/SearchResultService.php | 80 +++++++++++++++++-- .../Private/Partials/Product/Teaser.html | 4 + .../Private/Partials/Search/Result.html | 33 ++++++-- 4 files changed, 135 insertions(+), 33 deletions(-) diff --git a/public/typo3conf/ext/ep_products/Classes/Service/FilterService.php b/public/typo3conf/ext/ep_products/Classes/Service/FilterService.php index dafaa1eb..b22bb088 100644 --- a/public/typo3conf/ext/ep_products/Classes/Service/FilterService.php +++ b/public/typo3conf/ext/ep_products/Classes/Service/FilterService.php @@ -171,9 +171,9 @@ class FilterService implements SingletonInterface $dateMin = $item['minDateStart']; } if (null === $dateMax || $item['maxDateEnd'] > $dateMax) { - $dateMax = $row['itemMaxDateEnd']; + $dateMax = $item['maxDateEnd']; } - if ((bool) $row['busAvailable'] === true) { + if ((bool) $item['busAvailable'] === true) { $busAvailable = true; } foreach ($item['nights'] as $nightsOption) { @@ -182,36 +182,47 @@ class FilterService implements SingletonInterface $nights[] = $duration; } } - if (!array_key_exists($item['country']['uid'], $destinations)) { - $destinations[$item['country']['uid']] = [ + $itemDestinations = $item['filterMetadata']['destinations'] ?? [[ + 'country' => $item['country'], + 'region' => $item['region'], + ]]; + foreach ($itemDestinations as $itemDestination) { + $country = $itemDestination['country']; + $region = $itemDestination['region']; + if (!array_key_exists($country['uid'], $destinations)) { + $destinations[$country['uid']] = [ 'country' => [ - 'uid' => $item['country']['uid'], - 'label' => $item['country']['name'], - 'code' => $item['country']['code'], + 'uid' => $country['uid'], + 'label' => $country['name'], + 'code' => $country['code'], ], 'regions' => [], - ]; + ]; + } + if (!array_key_exists($region['uid'], $destinations[$country['uid']]['regions'])) { + $destinations[$country['uid']]['regions'][$region['uid']] = [ + 'uid' => $region['uid'], + 'label' => $region['name'], + ]; + } } - if (!array_key_exists($item['region']['uid'], $destinations[$item['country']['uid']]['regions'])) { - $destinations[$item['country']['uid']]['regions'][$item['region']['uid']] = [ - 'uid' => $item['region']['uid'], - 'label' => $item['region']['name'], - ]; - } - if (!\in_array($item['hotel']['type'], $hotelTypes, false)) { - $hotelTypes[] = $item['hotel']['type']; + foreach ($item['filterMetadata']['hotelTypes'] ?? [$item['hotel']['type']] as $hotelType) { + if (!\in_array($hotelType, $hotelTypes, false)) { + $hotelTypes[] = $hotelType; + } } foreach ($item['rooms'] as $roomType) { if (!\in_array($roomType, $roomTypes, false)) { $roomTypes[] = $roomType; } } - $boardType = [ + foreach ($item['filterMetadata']['boardTypes'] ?? [[ 'uid' => $item['boardUid'], 'label' => $item['board'], - ]; - if (!\in_array($boardType, $boardTypes, false)) { - $boardTypes[] = $boardType; + ]] as $boardType) { + if (!\in_array($boardType, $boardTypes, false)) { + $boardTypes[] = $boardType; + } } foreach (FilterOptions::$priceRangeOptions as $index => $priceRange) { if (array_key_exists($index, $priceRanges)) { diff --git a/public/typo3conf/ext/ep_products/Classes/Service/SearchResultService.php b/public/typo3conf/ext/ep_products/Classes/Service/SearchResultService.php index 56a7cd0a..21e7a88c 100644 --- a/public/typo3conf/ext/ep_products/Classes/Service/SearchResultService.php +++ b/public/typo3conf/ext/ep_products/Classes/Service/SearchResultService.php @@ -49,14 +49,25 @@ class SearchResultService implements SingletonInterface */ protected $hotelImageService; + /** + * @var ProductImageService + */ + protected $productImageService; + /** * @param ProductRepository $productRepository * @param HotelImageService $hotelImageService + * @param ProductImageService $productImageService */ - public function __construct(ProductRepository $productRepository, HotelImageService $hotelImageService) + public function __construct( + ProductRepository $productRepository, + HotelImageService $hotelImageService, + ProductImageService $productImageService + ) { $this->productRepository = $productRepository; $this->hotelImageService = $hotelImageService; + $this->productImageService = $productImageService; } public function getNormalizedSearchParams(array $searchParams = []): array @@ -80,10 +91,10 @@ class SearchResultService implements SingletonInterface { $searchResultData = $this->productRepository->getSearchResult($filterSettings, $excludedConceptUids); - return $this->preprocess($searchResultData); + return $this->preprocess($searchResultData, true); } - public function preprocess(array $searchResultData): SearchResult + public function preprocess(array $searchResultData, bool $aggregateEvents = false): SearchResult { $data = []; $total = 0; @@ -105,8 +116,13 @@ class SearchResultService implements SingletonInterface $items =& $data[$row['conceptUid']]['items']; - // Use compound key, hotel is not unique - $key = $row['hotelUid'] . ':' . $row['productUid']; + $isAggregatedEvent = $aggregateEvents && 'evt' === $row['conceptCode']; + + // Events are product-level search results. Other results (including + // watchlist items) remain tied to a particular hotel. + $key = $isAggregatedEvent + ? (string) $row['productUid'] + : $row['hotelUid'] . ':' . $row['productUid']; $hotelCategory = Hotel::$categoryLabels[$row['hotelCategory']] ?? null; @@ -122,6 +138,7 @@ class SearchResultService implements SingletonInterface 'season' => $row['season'], 'hideBookingButton' => $row['hideBookingButton'], 'busAvailable' => $row['busAvailable'], + 'available' => $row['available'], 'lowContingent' => $row['lowContingent'], 'servicesIncluded' => unserialize($row['servicesIncluded'], ['allowed_classes' => false]), 'hotel' => [ @@ -151,21 +168,55 @@ class SearchResultService implements SingletonInterface 'subline' => $row['productSubline'], 'feature' => $row['productFeature'], 'fact' => $row['productFact'], + 'images' => $isAggregatedEvent + ? $this->productImageService->getImages($row['productUid']) + : [], ], 'board' => $row['board'], 'boardUid' => $row['boardUid'], 'dates' => [], 'nights' => [], + 'teaserNights' => [], 'rooms' => [], 'new' => $row['new'], 'skiPassIncluded' => $row['skiPassIncluded'], + 'filterMetadata' => [ + 'destinations' => [], + 'hotelTypes' => [], + 'boardTypes' => [], + ], ]; $total++; } $item =& $items[$key]; - if (!\in_array($row['roomType'], $item['rooms'], false)) { + $destination = [ + 'country' => [ + 'uid' => $row['countryUid'], + 'code' => $row['countryCode'], + 'name' => $row['countryName'], + ], + 'region' => [ + 'uid' => $row['regionUid'], + 'name' => $row['regionName'], + 'fact' => $row['regionFact'], + 'feature' => $row['regionFeature'], + ], + ]; + if (!\in_array($destination, $item['filterMetadata']['destinations'], false)) { + $item['filterMetadata']['destinations'][] = $destination; + } + $hotelType = $row['hotelType'] ?? $item['hotel']['type']; + if (!\in_array($hotelType, $item['filterMetadata']['hotelTypes'], false)) { + $item['filterMetadata']['hotelTypes'][] = $hotelType; + } + $boardType = ['uid' => $row['boardUid'] ?? null, 'label' => $row['board']]; + if (!\in_array($boardType, $item['filterMetadata']['boardTypes'], false)) { + $item['filterMetadata']['boardTypes'][] = $boardType; + } + + if (isset($row['roomType']) && !\in_array($row['roomType'], $item['rooms'], false)) { $item['rooms'][] = $row['roomType']; } @@ -193,8 +244,25 @@ class SearchResultService implements SingletonInterface if ($item['minPrice'] === null || $item['minPrice'] > $row['minPrice']) { $item['minPrice'] = $row['minPrice']; + // Teaser-specific offer data must describe the cheapest row. + $item['teaserNights'] = [$row['nights']]; + $item['board'] = $row['board']; + $item['boardUid'] = $row['boardUid']; + $item['daytrip'] = $row['daytrip']; + $item['season'] = $row['season']; + $item['hideBookingButton'] = $row['hideBookingButton']; + $item['servicesIncluded'] = unserialize($row['servicesIncluded'], ['allowed_classes' => false]); + $item['new'] = $row['new']; + $item['skiPassIncluded'] = $row['skiPassIncluded']; } + // Availability indicators describe the consolidated set, not only + // the row that supplies the teaser price. + $item['earlybird'] = (bool) $item['earlybird'] || (bool) $row['earlybird']; + $item['busAvailable'] = (bool) $item['busAvailable'] || (bool) $row['busAvailable']; + $item['lowContingent'] = (bool) $item['lowContingent'] || (bool) $row['lowContingent']; + $item['available'] = (bool) $item['available'] || (bool) $row['available']; + } // Sort products by minprice within concept sections except for 'event' concept diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html index 73e8a902..40bd1bb8 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Product/Teaser.html @@ -38,6 +38,8 @@ {concept.name} + + + +
diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html index 919b17e2..348b16ad 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html @@ -64,24 +64,43 @@
+ data-teasergrid-target="container"> + + + + + + + + + + + + + + + + + +