feat: display included services in dates view

addresses #869c93zxj
This commit is contained in:
Björn Fromme
2026-06-15 08:32:04 +02:00
parent a8e915ae38
commit 432b47f38c
3 changed files with 146 additions and 38 deletions
@@ -176,6 +176,7 @@ class AjaxTableController extends ActionController
$bookableDates = array_filter($allDates, function (array $date) {
return $date['available'] > 0;
});
$includedServices = $this->dateService->groupIncludedServices($bookableDates);
$altProductLink = null;
$altLabel = null;
@@ -205,6 +206,8 @@ class AjaxTableController extends ActionController
$this->view->assignMultiple([
'allDates' => $allDates,
'bookableDates' => $bookableDates,
'includedServicesAllDates' => $includedServices['allDates'],
'includedServicesSomeDates' => $includedServices['someDates'],
'bookable' => $product->getBookable(),
'unavailable' => 0 === count($allDates),
'product' => $product,
@@ -442,6 +442,70 @@ class DateService implements SingletonInterface
return false;
}
/**
* Group included services across bookable dates.
*
* Returns services that appear in every supplied date and services that
* appear in at least one supplied date but not all of them.
*
* @param array $dates
* @return array{allDates: array, someDates: array}
*/
public function groupIncludedServices(array $dates): array
{
$serviceCounts = [];
$serviceOrder = [];
$dateCount = count($dates);
foreach ($dates as $date) {
$dateServices = $date['servicesIncluded'] ?? [];
if (!is_array($dateServices)) {
continue;
}
$seenInDate = [];
foreach ($dateServices as $service) {
if (!is_string($service) || '' === trim($service)) {
continue;
}
if (isset($seenInDate[$service])) {
continue;
}
$seenInDate[$service] = true;
if (!array_key_exists($service, $serviceCounts)) {
$serviceCounts[$service] = 0;
$serviceOrder[] = $service;
}
$serviceCounts[$service]++;
}
}
$groupedServices = [
'allDates' => [],
'someDates' => [],
];
if (0 === $dateCount) {
return $groupedServices;
}
foreach ($serviceOrder as $service) {
$count = $serviceCounts[$service] ?? 0;
if ($dateCount === $count) {
$groupedServices['allDates'][] = $service;
} elseif ($count > 0) {
$groupedServices['someDates'][] = $service;
}
}
sort($groupedServices['someDates'], SORT_NATURAL | SORT_FLAG_CASE);
return $groupedServices;
}
public function checkSurcharcheOrDiscount(array $tableData): array
{
$data = [
@@ -176,69 +176,110 @@
</tbody>
</table>
<ul class="divide-y divide-zinc-200">
<ul class="divide-y divide-zinc-200 mb-4">
<f:if condition="{hasSurcharge}">
<li class="py-2">
<button type="button"
data-action="pricetable#toggleSurcharge"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="surchargeToggleLabel"
data-label-default="Preise für Eigenanreise anzeigen"
data-label-alt="Preise für Busanreise anzeigen">
<button type="button"
data-action="pricetable#toggleSurcharge"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="surchargeToggleLabel"
data-label-default="Preise für Eigenanreise anzeigen"
data-label-alt="Preise für Busanreise anzeigen">
Preise für Eigenanreise anzeigen
</span>
</button>
</button>
</li>
</f:if>
<f:if condition="{hasDiscount}">
<li class="py-2">
<button type="button"
data-action="pricetable#toggleDiscount"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="discountToggleLabel"
data-label-default="Preise für Busanreise anzeigen"
data-label-alt="Preise für Eigenanreise anzeigen">
<button type="button"
data-action="pricetable#toggleDiscount"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="discountToggleLabel"
data-label-default="Preise für Busanreise anzeigen"
data-label-alt="Preise für Eigenanreise anzeigen">
Preise für Busanreise anzeigen
</span>
</button>
</button>
</li>
</f:if>
<f:if condition="{bookableDates -> f:count()} > 5">
<li class="py-2">
<button type="button"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark"
data-action="pricetable#toggleRows">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-plus-circle"
data-pricetable-target="rowToggleIcon"></use>
</svg>
<span data-pricetable-target="rowToggleLabel">
<button type="button"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark"
data-action="pricetable#toggleRows">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-plus-circle"
data-pricetable-target="rowToggleIcon"></use>
</svg>
<span data-pricetable-target="rowToggleLabel">
Alle Termine anzeigen
</span>
</button>
</button>
</li>
</f:if>
<f:if condition="{altProductLink}">
<li class="py-2">
<a href="{altProductLink}"
title="{altLabel}"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span>
<a href="{altProductLink}"
title="{altLabel}"
class="flex items-center space-x-1 focus:outline-none text-ep-primary-dark ser:text-ser-primary-dark">
<svg class="w-6 h-6">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-arrow-circle-right"></use>
</svg>
<span>
{altLabel}
</span>
</a>
</a>
</li>
</f:if>
</ul>
<f:if condition="{includedServicesAllDates}">
<div class="headline--3 headline--underlined">
Inklusivleistungen
</div>
<div class="bg-zinc-50 p-4 mb-8">
<ul class="grid gap-2 sm:grid-cols-2">
<f:for each="{includedServicesAllDates}" as="service">
<li class="flex items-start space-x-2">
<svg class="w-5 h-5 mt-0.5 flex-none text-ep-primary-dark ser:text-ser-primary-dark">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-check"></use>
</svg>
<span class="text-sm md:text-base">
{service}
</span>
</li>
</f:for>
</ul>
</div>
</f:if>
<f:if condition="{includedServicesSomeDates}">
<div class="headline--3 headline--underlined">
Teilweise inklusive
</div>
<div class="bg-zinc-50 p-4 mb-8">
<ul class="grid gap-2 sm:grid-cols-2">
<f:for each="{includedServicesSomeDates}" as="service">
<li class="flex items-start space-x-2">
<svg class="w-5 h-5 mt-0.5 flex-none text-ep-primary-dark ser:text-ser-primary-dark">
<use href="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/icons_sprite.svg')}#icon-check"></use>
</svg>
<span class="text-sm md:text-base">
{service}
</span>
</li>
</f:for>
</ul>
</div>
</f:if>
</f:if>
</div>