feat: display pickups data on product detail page
addresses #869cgn2fm
This commit is contained in:
1 parent
c6ab02305b
commit
41d188a8cc
6 files changed
+210
-5
No files matched your search
@@ -36,6 +36,7 @@ use EP\EpProducts\Service\ContingentDataService;
|
||||
use EP\EpProducts\Service\DateService;
|
||||
use EP\EpProducts\Service\FilterService;
|
||||
use EP\EpProducts\Service\HotelDataService;
|
||||
use EP\EpProducts\Service\PickupOverviewService;
|
||||
use EP\EpProducts\Service\ProductDataService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
@@ -82,6 +83,11 @@ class ProductController extends ActionController
|
||||
*/
|
||||
private $infoboxRepository;
|
||||
|
||||
/**
|
||||
* @var PickupOverviewService
|
||||
*/
|
||||
private $pickupOverviewService;
|
||||
|
||||
/**
|
||||
* @param ProductRepository $productRepository
|
||||
* @param ProductDataService $productDataService
|
||||
@@ -91,6 +97,7 @@ class ProductController extends ActionController
|
||||
* @param DateService $dateService
|
||||
* @param FilterService $filterService
|
||||
* @param InfoboxRepository $infoboxRepository
|
||||
* @param PickupOverviewService $pickupOverviewService
|
||||
*/
|
||||
public function __construct(
|
||||
ProductRepository $productRepository,
|
||||
@@ -100,7 +107,8 @@ class ProductController extends ActionController
|
||||
ContingentDataService $contingentDataService,
|
||||
DateService $dateService,
|
||||
FilterService $filterService,
|
||||
InfoboxRepository $infoboxRepository
|
||||
InfoboxRepository $infoboxRepository,
|
||||
PickupOverviewService $pickupOverviewService
|
||||
) {
|
||||
$this->productRepository = $productRepository;
|
||||
$this->productDataService = $productDataService;
|
||||
@@ -110,6 +118,7 @@ class ProductController extends ActionController
|
||||
$this->dateService = $dateService;
|
||||
$this->filterService = $filterService;
|
||||
$this->infoboxRepository = $infoboxRepository;
|
||||
$this->pickupOverviewService = $pickupOverviewService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,6 +205,7 @@ class ProductController extends ActionController
|
||||
'referringUrl' => $backlinkUrl,
|
||||
'backlinkLabel' => $backLinkLabel,
|
||||
'nonBookableInfo' => $nonBookableInfo,
|
||||
'pickupsOverview' => $this->pickupOverviewService->getOverview($product, $hotel),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace EP\EpProducts\Domain\Repository;
|
||||
|
||||
use EP\EpProducts\Domain\Model\Product;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
|
||||
class DateRepository extends AbstractRepository
|
||||
{
|
||||
@@ -18,4 +19,26 @@ class DateRepository extends AbstractRepository
|
||||
->execute()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] serialized pickups of each upcoming date, latest first
|
||||
*/
|
||||
public function findPickupsForProductAndHotel(int $productUid, int $hotelUid): array
|
||||
{
|
||||
$qb = $this->getDbConnection()->createQueryBuilder();
|
||||
|
||||
return $qb
|
||||
->select('date.pickups')
|
||||
->from('tx_epproducts_domain_model_date', 'date')
|
||||
->where(
|
||||
$qb->expr()->eq('date.product', $qb->createNamedParameter($productUid, Connection::PARAM_INT)),
|
||||
$qb->expr()->eq('date.hotel', $qb->createNamedParameter($hotelUid, Connection::PARAM_INT)),
|
||||
'date.date_start >= NOW()'
|
||||
)
|
||||
->orderBy('date.date_start', 'DESC')
|
||||
->addOrderBy('date.uid', 'DESC')
|
||||
->execute()
|
||||
->fetchFirstColumn()
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Service;
|
||||
|
||||
use EP\EpProducts\Domain\Model\Hotel;
|
||||
use EP\EpProducts\Domain\Model\Product;
|
||||
use EP\EpProducts\Domain\Repository\DateRepository;
|
||||
|
||||
/**
|
||||
* Lists the pickup points of a product's latest upcoming date that has any,
|
||||
* with that date's time and round-trip surcharge per pickup point. Pickups,
|
||||
* times and prices may differ on other dates; taking the latest one keeps the
|
||||
* values unique and shows them for as long as the product is offered.
|
||||
*
|
||||
* The returned shape is meant to be served by MyE&P later on, so that only the
|
||||
* data source has to change once the local date table goes away.
|
||||
*/
|
||||
class PickupOverviewService
|
||||
{
|
||||
/**
|
||||
* @var DateRepository
|
||||
*/
|
||||
private $dateRepository;
|
||||
|
||||
public function __construct(DateRepository $dateRepository)
|
||||
{
|
||||
$this->dateRepository = $dateRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{pickups: array<int, array{busProId: int, city: string, street: string, price: float, time: string}>}
|
||||
*/
|
||||
public function getOverview(Product $product, ?Hotel $hotel): array
|
||||
{
|
||||
$overview = ['pickups' => []];
|
||||
if ($hotel === null || $product->isDaytrip()) {
|
||||
return $overview;
|
||||
}
|
||||
|
||||
$dates = $this->dateRepository->findPickupsForProductAndHotel((int) $product->getUid(), (int) $hotel->getUid());
|
||||
foreach ($dates as $serializedPickups) {
|
||||
$pickups = $this->readPickups((string) $serializedPickups);
|
||||
if ($pickups !== []) {
|
||||
$overview['pickups'] = $pickups;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $overview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the order of the import, which is the boarding order.
|
||||
*/
|
||||
private function readPickups(string $serializedPickups): array
|
||||
{
|
||||
$datePickups = @unserialize($serializedPickups, ['allowed_classes' => false]);
|
||||
if (!is_array($datePickups)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$pickups = [];
|
||||
foreach ($datePickups as $pickup) {
|
||||
$busProId = (int) ($pickup['bus_pro_id'] ?? 0);
|
||||
if ($busProId === 0 || isset($pickups[$busProId])) {
|
||||
continue;
|
||||
}
|
||||
$pickups[$busProId] = [
|
||||
'busProId' => $busProId,
|
||||
'city' => (string) ($pickup['city'] ?? ''),
|
||||
'street' => (string) ($pickup['street'] ?? ''),
|
||||
// A pickup's price is one leg; the surcharge is split evenly between out- and inbound.
|
||||
'price' => 2 * (float) ($pickup['price'] ?? 0.0),
|
||||
'time' => (string) ($pickup['time'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return array_values($pickups);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [ 'sizer', 'container', 'button' ]
|
||||
static targets = [ 'sizer', 'container', 'button', 'bar' ]
|
||||
|
||||
static values = {
|
||||
offset: {
|
||||
@@ -33,6 +33,12 @@ export default class extends Controller {
|
||||
// Set initial expanded/collapsed status and button visibility
|
||||
this.expandedValue = this.contentHeight < this.maxHeight
|
||||
this.buttonTarget.classList.toggle('hidden', this.expandedValue)
|
||||
|
||||
// Drop the space reserved for the toggle bar when the content fits anyway
|
||||
if (this.expandedValue && this.hasBarTarget) {
|
||||
this.barTarget.classList.add('hidden')
|
||||
this.containerTarget.style.paddingBottom = '0'
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<div class="group odd:bg-zinc-50 p-4" data-rte-content>
|
||||
<h3>
|
||||
Bus-Zustiege
|
||||
</h3>
|
||||
<f:if condition="{pickupsOverview.pickups -> f:count()} <= 3">
|
||||
<f:then>
|
||||
<f:render section="Table" arguments="{_all}"/>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<div data-controller="autocollapse" data-autocollapse-height-value="400" data-autocollapse-offset-value="64">
|
||||
<div class="relative pb-12" data-autocollapse-target="container">
|
||||
<f:render section="Table" arguments="{_all}"/>
|
||||
<div class="absolute left-0 bottom-0 w-full h-12 bg-white group-odd:bg-zinc-50 flex items-center" data-autocollapse-target="bar">
|
||||
<button type="button"
|
||||
data-autocollapse-target="button"
|
||||
data-action="autocollapse#toggle"
|
||||
class="hidden text-ep-primary w-10 h-10 ml-2 group toggle-more">
|
||||
<f:render section="ToggleIcon"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<p class="text-sm">
|
||||
Abfahrtszeiten und -orte unter Vorbehalt. Die genauen Zeiten werden euch kurz vor der Anreise mitgeteilt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<f:section name="Table">
|
||||
<table class="w-full bg-zinc-200 text-zinc-800 text-xs md:text-base mb-4">
|
||||
<tr>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base">
|
||||
Zustieg
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base">
|
||||
Abfahrt
|
||||
</th>
|
||||
<th class="border-r border-white/50 px-2 py-1 md:p-2 text-left text-sm md:text-base">
|
||||
Zuschlag
|
||||
</th>
|
||||
</tr>
|
||||
<f:for each="{pickupsOverview.pickups}" as="pickup">
|
||||
<tr class="odd:bg-zinc-50 align-top">
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2">
|
||||
{pickup.city}<br><span class="text-xs">{pickup.street}</span>
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 whitespace-nowrap">
|
||||
<f:if condition="{pickup.time}">
|
||||
<f:then>{pickup.time} Uhr</f:then>
|
||||
<f:else>-</f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
<td class="border-r border-zinc-200 px-2 py-1 md:p-2 whitespace-nowrap">
|
||||
<f:if condition="{pickup.price} > 0">
|
||||
<f:then>{pickup.price -> f:format.currency(currencySign: '€', decimalSeparator: ',', thousandsSeparator: '.')}</f:then>
|
||||
<f:else>-</f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
</tr>
|
||||
</f:for>
|
||||
</table>
|
||||
</f:section>
|
||||
|
||||
<f:section name="ToggleIcon">
|
||||
<svg viewBox="0 0 184 185" xmlns="http://www.w3.org/2000/svg" stroke="none" fill="currentColor">
|
||||
<g>
|
||||
<path d="M92,0C41.2,0 0,41.2 0,92.1C0,143 41.2,184.1 92,184.1C142.8,184.1 184,142.9 184,92.1C184,41.3 142.8,0 92,0ZM92,173.1C47.3,173.1 11,136.8 11,92.1C11,47.4 47.3,11.1 92,11.1C136.7,11.1 173,47.4 173,92.1C173,136.8 136.7,173.1 92,173.1Z"/>
|
||||
<path d="M134.2,92.1C134.2,93.6 133.7,95 132.6,96C131.5,97.1 130.3,97.6 128.7,97.6L55.3,97.6C53.8,97.6 52.4,97.1 51.4,96C50.3,94.9 49.8,93.7 49.8,92.1C49.8,90.5 50.3,89.2 51.4,88.2C52.5,87.1 53.7,86.6 55.3,86.6L128.6,86.6C130.1,86.6 131.5,87.1 132.5,88.2C133.6,89.3 134.1,90.5 134.1,92.1"/>
|
||||
</g>
|
||||
<g class="group-[.toggle-less]:hidden">
|
||||
<path d="M97,128.7C97,130.2 96.5,131.6 95.4,132.6C94.3,133.7 93.1,134.2 91.5,134.2C89.9,134.2 88.6,133.7 87.6,132.6C86.5,131.5 86,130.3 86,128.7L86,55.4C86,53.9 86.5,52.5 87.6,51.5C88.7,50.4 89.9,49.9 91.5,49.9C93.1,49.9 94.4,50.4 95.4,51.5C96.5,52.6 97,53.8 97,55.4L97,128.7Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,7 @@
|
||||
<f:render section="TabButton" arguments="{tab: 'hotel', label: 'Unterkunft', wide: 0}"/>
|
||||
<f:render section="TabButton" arguments="{tab: 'region', label: 'Gebiet', wide: 0}"/>
|
||||
<f:render section="TabButton" arguments="{tab: 'city', label: 'Ort', wide: 0}"/>
|
||||
<f:if condition="{product.journey}">
|
||||
<f:if condition="{product.journey} || {pickupsOverview.pickups}">
|
||||
<f:render section="TabButton" arguments="{tab: 'journey', label: 'Anreise', wide: 0}"/>
|
||||
</f:if>
|
||||
</div>
|
||||
@@ -158,11 +158,16 @@
|
||||
</f:section>
|
||||
|
||||
<f:section name="Journey">
|
||||
<f:if condition="{product.journey}">
|
||||
<f:if condition="{product.journey} || {pickupsOverview.pickups}">
|
||||
<div class="mb-4 hidden"
|
||||
data-tabs-target="tab"
|
||||
data-tab="journey">
|
||||
<f:render partial="Journey/Details" arguments="{journey: product.journey, bars: 1}"/>
|
||||
<f:if condition="{product.journey}">
|
||||
<f:render partial="Journey/Details" arguments="{journey: product.journey, bars: 1}"/>
|
||||
</f:if>
|
||||
<f:if condition="{pickupsOverview.pickups}">
|
||||
<f:render partial="Product/Pickups" arguments="{pickupsOverview: pickupsOverview}"/>
|
||||
</f:if>
|
||||
</div>
|
||||
</f:if>
|
||||
</f:section>
|
||||
|
||||
Reference in new issue
Block a user